-Reverted manual track selection from exoplayer track selector.

-Added quality record to play queue items.
-Added quality and recovery record play queue events.
-Added landscape view for ServicePlayerActivity.
-Moved repeat and shuffle button to play queue panel in main video player.
-Fixed potential NPE in MediaSourceManager by no longer nulling play queue on dispose.
-Renamed PlayQueueEvent to PlayQueueEventType.
-Renamed PlayQueueMessage to PlayQueueEvent.
This commit is contained in:
John Zhen Mo 2017-10-22 18:58:01 -07:00
parent 4553850412
commit 9068247856
32 changed files with 696 additions and 358 deletions

View File

@ -788,16 +788,14 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream()); ((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream());
} }
final PlayQueue playQueue = new SinglePlayQueue(currentInfo); final PlayQueue playQueue = new SinglePlayQueue(currentInfo, actionBarHandler.getSelectedVideoStream());
final VideoStream candidate = sortedStreamVideosList.get(actionBarHandler.getSelectedVideoStream());
final Intent intent; final Intent intent;
if (append) { if (append) {
Toast.makeText(activity, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
intent = NavigationHelper.getPlayerIntent(activity, PopupVideoPlayer.class, playQueue, true); intent = NavigationHelper.getPlayerEnqueueIntent(activity, PopupVideoPlayer.class, playQueue);
} else { } else {
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
intent = NavigationHelper.getPlayerIntent(activity, PopupVideoPlayer.class, playQueue, Localization.resolutionOf(candidate.resolution)); intent = NavigationHelper.getPlayerIntent(activity, PopupVideoPlayer.class, playQueue);
} }
activity.startService(intent); activity.startService(intent);
} }
@ -819,10 +817,11 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
private void openNormalBackgroundPlayer(final boolean append) { private void openNormalBackgroundPlayer(final boolean append) {
final PlayQueue playQueue = new SinglePlayQueue(currentInfo); final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
activity.startService(NavigationHelper.getPlayerIntent(activity, BackgroundPlayer.class, playQueue, append));
if (append) { if (append) {
activity.startService(NavigationHelper.getPlayerEnqueueIntent(activity, BackgroundPlayer.class, playQueue));
Toast.makeText(activity, R.string.background_player_append, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.background_player_append, Toast.LENGTH_SHORT).show();
} else { } else {
activity.startService(NavigationHelper.getPlayerIntent(activity, BackgroundPlayer.class, playQueue));
Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show();
} }
} }
@ -867,9 +866,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|| (Build.VERSION.SDK_INT < 16); || (Build.VERSION.SDK_INT < 16);
if (!useOldPlayer) { if (!useOldPlayer) {
// ExoPlayer // ExoPlayer
final PlayQueue playQueue = new SinglePlayQueue(currentInfo); final PlayQueue playQueue = new SinglePlayQueue(currentInfo, actionBarHandler.getSelectedVideoStream());
final VideoStream candidate = sortedStreamVideosList.get(actionBarHandler.getSelectedVideoStream()); mIntent = NavigationHelper.getPlayerIntent(activity, MainVideoPlayer.class, playQueue);
mIntent = NavigationHelper.getPlayerIntent(activity, MainVideoPlayer.class, playQueue, Localization.resolutionOf(candidate.resolution));
} else { } else {
// Internal Player // Internal Player
mIntent = new Intent(activity, PlayVideoActivity.class) mIntent = new Intent(activity, PlayVideoActivity.class)

View File

@ -1,11 +1,13 @@
package org.schabi.newpipe.fragments.list.playlist; package org.schabi.newpipe.fragments.list.playlist;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -14,6 +16,7 @@ import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListExtractor;
@ -29,6 +32,7 @@ import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import io.reactivex.Single; import io.reactivex.Single;
@ -162,6 +166,13 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
headerPopupButton.setOnClickListener(new View.OnClickListener() { headerPopupButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(activity)) {
Toast toast = Toast.makeText(activity, R.string.msg_popup_permission, Toast.LENGTH_LONG);
TextView messageView = toast.getView().findViewById(android.R.id.message);
if (messageView != null) messageView.setGravity(Gravity.CENTER);
toast.show();
return;
}
activity.startService(buildPlaylistIntent(PopupVideoPlayer.class)); activity.startService(buildPlaylistIntent(PopupVideoPlayer.class));
} }
}); });

View File

@ -290,7 +290,9 @@ public final class BackgroundPlayer extends Service {
} }
@Override @Override
protected void postProcess(@NonNull final Intent intent) { public void handleIntent(final Intent intent) {
super.handleIntent(intent);
resetNotification(); resetNotification();
startForeground(NOTIFICATION_ID, notBuilder.build()); startForeground(NOTIFICATION_ID, notBuilder.build());
@ -437,7 +439,7 @@ public final class BackgroundPlayer extends Service {
} }
@Override @Override
public MediaSource sourceOf(final StreamInfo info) { public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams); final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
if (index < 0) return null; if (index < 0) return null;

View File

@ -122,6 +122,8 @@ public abstract class BasePlayer implements Player.EventListener,
// Intent // Intent
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final String REPEAT_MODE = "repeat_mode";
public static final String PLAYBACK_PITCH = "playback_pitch";
public static final String PLAYBACK_SPEED = "playback_speed"; public static final String PLAYBACK_SPEED = "playback_speed";
public static final String PLAY_QUEUE = "play_queue"; public static final String PLAY_QUEUE = "play_queue";
public static final String APPEND_ONLY = "append_only"; public static final String APPEND_ONLY = "append_only";
@ -234,8 +236,6 @@ public abstract class BasePlayer implements Player.EventListener,
}); });
} }
protected abstract void postProcess(@NonNull final Intent intent);
public void handleIntent(Intent intent) { public void handleIntent(Intent intent) {
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
if (intent == null) return; if (intent == null) return;
@ -253,6 +253,7 @@ public abstract class BasePlayer implements Player.EventListener,
} }
setPlaybackSpeed(intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed())); setPlaybackSpeed(intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()));
setPlaybackPitch(intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch()));
// Re-initialization // Re-initialization
destroyPlayer(); destroyPlayer();
@ -262,7 +263,6 @@ public abstract class BasePlayer implements Player.EventListener,
// Good to go... // Good to go...
initPlayback(this, queue); initPlayback(this, queue);
postProcess(intent);
} }
protected void initPlayback(@NonNull final PlaybackListener listener, @NonNull final PlayQueue queue) { protected void initPlayback(@NonNull final PlaybackListener listener, @NonNull final PlayQueue queue) {
@ -288,7 +288,6 @@ public abstract class BasePlayer implements Player.EventListener,
}); });
} }
public void onThumbnailReceived(Bitmap thumbnail) { public void onThumbnailReceived(Bitmap thumbnail) {
if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]"); if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]");
} }
@ -470,7 +469,6 @@ public abstract class BasePlayer implements Player.EventListener,
public static final int STATE_PAUSED_SEEK = 127; public static final int STATE_PAUSED_SEEK = 127;
public static final int STATE_COMPLETED = 128; public static final int STATE_COMPLETED = 128;
protected int currentState = -1; protected int currentState = -1;
public void changeState(int state) { public void changeState(int state) {
@ -577,15 +575,13 @@ public abstract class BasePlayer implements Player.EventListener,
// Check if recovering // Check if recovering
if (isCurrentWindowCorrect && currentSourceItem != null && if (isCurrentWindowCorrect && currentSourceItem != null &&
currentSourceItem.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { currentSourceItem.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
/* Recovering with sub-second position may cause a long buffer delay in ExoPlayer,
* rounding this position to the nearest second will help alleviate this.*/
final long position = currentSourceItem.getRecoveryPosition();
// todo: figure out exactly why this is the case if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + " at: " + getTimeString((int)position));
/* Rounding time to nearest second as certain media cannot guarantee a sub-second seek
will complete and the player might get stuck in buffering state forever */
final long roundedPos = (currentSourceItem.getRecoveryPosition() / 1000) * 1000;
if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + " at: " + getTimeString((int)roundedPos));
simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition()); simpleExoPlayer.seekTo(currentSourceItem.getRecoveryPosition());
currentSourceItem.resetRecoveryPosition(); playQueue.unsetRecovery(currentSourceIndex);
} }
} }
@ -995,10 +991,6 @@ public abstract class BasePlayer implements Player.EventListener,
simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch)); simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch));
} }
public int getCurrentResolutionTarget() {
return trackSelector != null ? trackSelector.getParameters().maxVideoHeight : Integer.MAX_VALUE;
}
public PlayQueue getPlayQueue() { public PlayQueue getPlayQueue() {
return playQueue; return playQueue;
} }
@ -1024,6 +1016,6 @@ public abstract class BasePlayer implements Player.EventListener,
if (playQueue.size() <= queuePos) return; if (playQueue.size() <= queuePos) return;
if (DEBUG) Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos); if (DEBUG) Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos);
playQueue.getItem(queuePos).setRecoveryPosition(windowPos); playQueue.setRecovery(queuePos, windowPos);
} }
} }

View File

@ -28,7 +28,6 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper; import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log; import android.util.Log;
@ -52,7 +51,6 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder; import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -208,6 +206,15 @@ public final class MainVideoPlayer extends Activity {
} }
} }
protected void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) {
final int shuffleAlpha = shuffled ? 255 : 77;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
shuffleButton.setImageAlpha(shuffleAlpha);
} else {
shuffleButton.setAlpha(shuffleAlpha);
}
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@SuppressWarnings({"unused", "WeakerAccess"}) @SuppressWarnings({"unused", "WeakerAccess"})
@ -216,8 +223,9 @@ public final class MainVideoPlayer extends Activity {
private TextView channelTextView; private TextView channelTextView;
private TextView volumeTextView; private TextView volumeTextView;
private TextView brightnessTextView; private TextView brightnessTextView;
private ImageButton repeatButton;
private ImageButton queueButton; private ImageButton queueButton;
private ImageButton repeatButton;
private ImageButton shuffleButton;
private ImageButton screenRotationButton; private ImageButton screenRotationButton;
private ImageButton playPauseButton; private ImageButton playPauseButton;
@ -242,8 +250,9 @@ public final class MainVideoPlayer extends Activity {
this.channelTextView = rootView.findViewById(R.id.channelTextView); this.channelTextView = rootView.findViewById(R.id.channelTextView);
this.volumeTextView = rootView.findViewById(R.id.volumeTextView); this.volumeTextView = rootView.findViewById(R.id.volumeTextView);
this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView); this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView);
this.repeatButton = rootView.findViewById(R.id.repeatButton);
this.queueButton = rootView.findViewById(R.id.queueButton); this.queueButton = rootView.findViewById(R.id.queueButton);
this.repeatButton = rootView.findViewById(R.id.repeatButton);
this.shuffleButton = rootView.findViewById(R.id.shuffleButton);
this.screenRotationButton = rootView.findViewById(R.id.screenRotationButton); this.screenRotationButton = rootView.findViewById(R.id.screenRotationButton);
this.playPauseButton = rootView.findViewById(R.id.playPauseButton); this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
@ -264,18 +273,14 @@ public final class MainVideoPlayer extends Activity {
queueButton.setOnClickListener(this); queueButton.setOnClickListener(this);
repeatButton.setOnClickListener(this); repeatButton.setOnClickListener(this);
shuffleButton.setOnClickListener(this);
playPauseButton.setOnClickListener(this); playPauseButton.setOnClickListener(this);
playPreviousButton.setOnClickListener(this); playPreviousButton.setOnClickListener(this);
playNextButton.setOnClickListener(this); playNextButton.setOnClickListener(this);
screenRotationButton.setOnClickListener(this); screenRotationButton.setOnClickListener(this);
} }
@Override
public int getPreferredResolution() {
if (sharedPreferences == null || context == null) return Integer.MAX_VALUE;
return Localization.resolutionOf(sharedPreferences.getString(context.getString(R.string.default_resolution_key), context.getString(R.string.default_resolution_value)));
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Video Listener // ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -283,7 +288,7 @@ public final class MainVideoPlayer extends Activity {
@Override @Override
public void onRepeatModeChanged(int i) { public void onRepeatModeChanged(int i) {
super.onRepeatModeChanged(i); super.onRepeatModeChanged(i);
setRepeatModeButton(repeatButton, simpleExoPlayer.getRepeatMode()); updatePlaybackButtons();
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -305,6 +310,12 @@ public final class MainVideoPlayer extends Activity {
playPauseButton.setImageResource(R.drawable.ic_pause_white); playPauseButton.setImageResource(R.drawable.ic_pause_white);
} }
@Override
public void onShuffleClicked() {
super.onShuffleClicked();
updatePlaybackButtons();
}
@Override @Override
public void onFullScreenButtonClicked() { public void onFullScreenButtonClicked() {
super.onFullScreenButtonClicked(); super.onFullScreenButtonClicked();
@ -323,8 +334,9 @@ public final class MainVideoPlayer extends Activity {
context, context,
PopupVideoPlayer.class, PopupVideoPlayer.class,
this.getPlayQueue(), this.getPlayQueue(),
this.getCurrentResolutionTarget(), this.simpleExoPlayer.getRepeatMode(),
this.getPlaybackSpeed() this.getPlaybackSpeed(),
this.getPlaybackPitch()
); );
context.startService(intent); context.startService(intent);
destroyPlayer(); destroyPlayer();
@ -336,10 +348,7 @@ public final class MainVideoPlayer extends Activity {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
super.onClick(v); super.onClick(v);
if (v.getId() == repeatButton.getId()) { if (v.getId() == playPauseButton.getId()) {
onRepeatClicked();
} else if (v.getId() == playPauseButton.getId()) {
onVideoPlayPause(); onVideoPlayPause();
} else if (v.getId() == playPreviousButton.getId()) { } else if (v.getId() == playPreviousButton.getId()) {
@ -354,6 +363,12 @@ public final class MainVideoPlayer extends Activity {
} else if (v.getId() == queueButton.getId()) { } else if (v.getId() == queueButton.getId()) {
onQueueClicked(); onQueueClicked();
return; return;
} else if (v.getId() == repeatButton.getId()) {
onRepeatClicked();
return;
} else if (v.getId() == shuffleButton.getId()) {
onShuffleClicked();
return;
} }
if (getCurrentState() != STATE_COMPLETED) { if (getCurrentState() != STATE_COMPLETED) {
@ -371,10 +386,14 @@ public final class MainVideoPlayer extends Activity {
private void onQueueClicked() { private void onQueueClicked() {
queueVisible = true; queueVisible = true;
buildQueue();
hideSystemUi(); hideSystemUi();
buildQueue();
updatePlaybackButtons();
getControlsRoot().setVisibility(View.INVISIBLE); getControlsRoot().setVisibility(View.INVISIBLE);
queueLayout.setVisibility(View.VISIBLE); queueLayout.setVisibility(View.VISIBLE);
itemsList.smoothScrollToPosition(playQueue.getIndex()); itemsList.smoothScrollToPosition(playQueue.getIndex());
} }
@ -527,12 +546,20 @@ public final class MainVideoPlayer extends Activity {
}, delay); }, delay);
} }
private void updatePlaybackButtons() {
if (repeatButton == null || shuffleButton == null ||
simpleExoPlayer == null || playQueue == null) return;
setRepeatModeButton(repeatButton, simpleExoPlayer.getRepeatMode());
setShuffleButton(shuffleButton, playQueue.isShuffled());
}
private void buildQueue() { private void buildQueue() {
queueLayout = findViewById(R.id.play_queue_control); queueLayout = findViewById(R.id.playQueuePanel);
itemsListCloseButton = findViewById(R.id.play_queue_close_area); itemsListCloseButton = findViewById(R.id.playQueueClose);
itemsList = findViewById(R.id.play_queue); itemsList = findViewById(R.id.playQueue);
itemsList.setAdapter(playQueueAdapter); itemsList.setAdapter(playQueueAdapter);
itemsList.setClickable(true); itemsList.setClickable(true);
itemsList.setLongClickable(true); itemsList.setLongClickable(true);

View File

@ -405,12 +405,6 @@ public final class PopupVideoPlayer extends Service {
resizingIndicator = rootView.findViewById(R.id.resizing_indicator); resizingIndicator = rootView.findViewById(R.id.resizing_indicator);
} }
@Override
public int getPreferredResolution() {
if (sharedPreferences == null || context == null) return Integer.MAX_VALUE;
return Localization.resolutionOf(sharedPreferences.getString(context.getString(R.string.default_popup_resolution_key), context.getString(R.string.default_popup_resolution_value)));
}
@Override @Override
public void destroy() { public void destroy() {
super.destroy(); super.destroy();
@ -443,8 +437,9 @@ public final class PopupVideoPlayer extends Service {
context, context,
MainVideoPlayer.class, MainVideoPlayer.class,
this.getPlayQueue(), this.getPlayQueue(),
this.getCurrentResolutionTarget(), this.simpleExoPlayer.getRepeatMode(),
this.getPlaybackSpeed() this.getPlaybackSpeed(),
this.getPlaybackPitch()
); );
if (!isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false); if (!isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

View File

@ -115,6 +115,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
bind(); bind();
} }
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_play_queue, menu); getMenuInflater().inflate(R.menu.menu_play_queue, menu);
@ -164,7 +169,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
serviceBound = false; serviceBound = false;
stopPlayerListener(); stopPlayerListener();
player = null; player = null;
finish();
} }
} }
@ -181,6 +185,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player = playerFrom(service); player = playerFrom(service);
if (player == null || player.playQueue == null || player.playQueueAdapter == null || player.simpleExoPlayer == null) { if (player == null || player.playQueue == null || player.playQueueAdapter == null || player.simpleExoPlayer == null) {
unbind(); unbind();
finish();
} else { } else {
buildComponents(); buildComponents();
startPlayerListener(); startPlayerListener();
@ -460,6 +465,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
@Override @Override
public void onServiceStopped() { public void onServiceStopped() {
unbind(); unbind();
finish();
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View File

@ -25,7 +25,6 @@ import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder; import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
@ -47,18 +46,10 @@ import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -90,7 +81,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe"; public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe";
public static final String MAX_RESOLUTION = "max_resolution";
private ArrayList<VideoStream> availableStreams; private ArrayList<VideoStream> availableStreams;
private int selectedStreamIndex; private int selectedStreamIndex;
@ -101,11 +91,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
private static final TrackSelection.Factory FIXED_FACTORY = new FixedTrackSelection.Factory();
private List<TrackGroupInfo> trackGroupInfos;
private TrackGroupArray videoTrackGroups;
private TrackGroup selectedVideoTrackGroup;
private boolean startedFromNewPipe = true; private boolean startedFromNewPipe = true;
protected boolean wasPlaying = false; protected boolean wasPlaying = false;
@ -130,7 +115,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
private SeekBar playbackSeekBar; private SeekBar playbackSeekBar;
private TextView playbackCurrentTime; private TextView playbackCurrentTime;
private TextView playbackEndTime; private TextView playbackEndTime;
private TextView playbackSpeed; private TextView playbackSpeedTextView;
private View topControlsRoot; private View topControlsRoot;
private TextView qualityTextView; private TextView qualityTextView;
@ -173,7 +158,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar); this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime); this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime); this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
this.playbackSpeed = rootView.findViewById(R.id.playbackSpeed); this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed);
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
this.topControlsRoot = rootView.findViewById(R.id.topControls); this.topControlsRoot = rootView.findViewById(R.id.topControls);
this.qualityTextView = rootView.findViewById(R.id.qualityTextView); this.qualityTextView = rootView.findViewById(R.id.qualityTextView);
@ -186,7 +171,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY);
this.qualityPopupMenu = new PopupMenu(context, qualityTextView); this.qualityPopupMenu = new PopupMenu(context, qualityTextView);
this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeed); this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView);
((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY);
@ -196,7 +181,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void initListeners() { public void initListeners() {
super.initListeners(); super.initListeners();
playbackSeekBar.setOnSeekBarChangeListener(this); playbackSeekBar.setOnSeekBarChangeListener(this);
playbackSpeed.setOnClickListener(this); playbackSpeedTextView.setOnClickListener(this);
fullScreenButton.setOnClickListener(this); fullScreenButton.setOnClickListener(this);
qualityTextView.setOnClickListener(this); qualityTextView.setOnClickListener(this);
} }
@ -212,78 +197,21 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
} }
} }
@Override
protected void postProcess(@NonNull final Intent intent) {
final int resolutionTarget = intent.getIntExtra(MAX_RESOLUTION, getPreferredResolution());
trackSelector.setParameters(
// Assume video is horizontal
new DefaultTrackSelector.Parameters().withMaxVideoSize(Integer.MAX_VALUE, resolutionTarget)
);
}
public abstract int getPreferredResolution();
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// UI Builders // UI Builders
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private final class TrackGroupInfo { public void buildQualityMenu() {
final int track; if (qualityPopupMenu == null) return;
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
|| availableStreams == null || videoTrackGroups.length != availableStreams.size()) return;
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
trackGroupInfos = new ArrayList<>(); for (int i = 0; i < availableStreams.size(); i++) {
int acc = 0; VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
// 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);
} }
qualityTextView.setText(getSelectedVideoStream().resolution);
trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, format));
acc++;
}
}
qualityPopupMenu.setOnMenuItemClickListener(this); qualityPopupMenu.setOnMenuItemClickListener(this);
qualityPopupMenu.setOnDismissListener(this); qualityPopupMenu.setOnDismissListener(this);
qualityTextView.setVisibility(View.VISIBLE);
} }
private void buildPlaybackSpeedMenu() { private void buildPlaybackSpeedMenu() {
@ -293,7 +221,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i])); playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i]));
} }
playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
playbackSpeedPopupMenu.setOnMenuItemClickListener(this); playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
playbackSpeedPopupMenu.setOnDismissListener(this); playbackSpeedPopupMenu.setOnDismissListener(this);
} }
@ -305,27 +233,46 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
@Override @Override
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
super.sync(item, info); super.sync(item, info);
qualityTextView.setVisibility(View.GONE);
playbackSpeedTextView.setVisibility(View.GONE);
if (info != null) { if (info != null) {
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
availableStreams = new ArrayList<>(videos); availableStreams = new ArrayList<>(videos);
final int qualityIndex = item.getQualityIndex();
if (qualityIndex == PlayQueueItem.DEFAULT_QUALITY) {
selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, videos); selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, videos);
} else {
selectedStreamIndex = qualityIndex;
} }
buildPlaybackSpeedMenu();
buildQualityMenu(); buildQualityMenu();
buildPlaybackSpeedMenu();
qualityTextView.setVisibility(View.VISIBLE);
playbackSpeedTextView.setVisibility(View.VISIBLE);
}
} }
public MediaSource sourceOf(final StreamInfo info) { public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false); final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false);
List<MediaSource> sources = new ArrayList<>(); final int sortedStreamsIndex = item.getQualityIndex();
if (videos.isEmpty() || sortedStreamsIndex >= videos.size()) return null;
for (final VideoStream video : videos) { final VideoStream video;
final MediaSource mediaSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format)); if (sortedStreamsIndex == PlayQueueItem.DEFAULT_QUALITY) {
sources.add(mediaSource); final int index = ListHelper.getDefaultResolutionIndex(context, videos);
video = videos.get(index);
} else {
video = videos.get(sortedStreamsIndex);
} }
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()])); final MediaSource streamSource = buildMediaSource(video.url, MediaFormat.getSuffixById(video.format));
final AudioStream audio = ListHelper.getHighestQualityAudio(info.audio_streams);
if (!video.isVideoOnly || audio == null) return streamSource;
// Merge with audio stream in case if video does not contain audio
final MediaSource audioSource = buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
return new MergingMediaSource(streamSource, audioSource);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -403,24 +350,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
// ExoPlayer Video Listener // ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
super.onTracksChanged(trackGroups, trackSelections);
if (trackSelector.getCurrentMappedTrackInfo() == null) return;
qualityTextView.setVisibility(View.GONE);
final int videoRendererIndex = getVideoRendererIndex();
if (videoRendererIndex == -1) return;
videoTrackGroups = trackSelector.getCurrentMappedTrackInfo().getTrackGroups(videoRendererIndex);
final TrackSelection trackSelection = trackSelections.get(videoRendererIndex);
if (trackSelection != null) {
selectedVideoTrackGroup = trackSelection.getTrackGroup();
buildQualityMenu();
}
}
@Override @Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (DEBUG) { if (DEBUG) {
@ -444,7 +373,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
super.onPrepared(playWhenReady); super.onPrepared(playWhenReady);
} }
@ -510,7 +439,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
onFullScreenButtonClicked(); onFullScreenButtonClicked();
} else if (v.getId() == qualityTextView.getId()) { } else if (v.getId() == qualityTextView.getId()) {
onQualitySelectorClicked(); onQualitySelectorClicked();
} else if (v.getId() == playbackSpeed.getId()) { } else if (v.getId() == playbackSpeedTextView.getId()) {
onPlaybackSpeedClicked(); onPlaybackSpeedClicked();
} }
} }
@ -524,34 +453,19 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]"); Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]");
if (qualityPopupMenuGroupId == menuItem.getGroupId()) { if (qualityPopupMenuGroupId == menuItem.getGroupId()) {
final int itemId = menuItem.getItemId(); if (selectedStreamIndex == menuItem.getItemId()) return true;
final TrackGroupInfo info = trackGroupInfos.get(itemId);
// Set selected quality as player lifecycle persistent parameters setRecovery();
DefaultTrackSelector.Parameters parameters; playQueue.setQuality(playQueue.getIndex(), menuItem.getItemId());
if (info.format.width > info.format.height) {
// Check if video horizontal
parameters = new DefaultTrackSelector.Parameters().withMaxVideoSize(Integer.MAX_VALUE, info.format.height);
} else {
// Or if vertical
parameters = new DefaultTrackSelector.Parameters().withMaxVideoSize(info.format.width, Integer.MAX_VALUE);
}
trackSelector.setParameters(parameters);
final int videoRendererIndex = getVideoRendererIndex();
if (videoRendererIndex != -1) {
// Override the selection with the selected quality in case of different frame rate
final MappingTrackSelector.SelectionOverride override = new MappingTrackSelector.SelectionOverride(FIXED_FACTORY, info.group, info.track);
trackSelector.setSelectionOverride(videoRendererIndex, videoTrackGroups, override);
}
qualityTextView.setText(menuItem.getTitle());
return true; return true;
} else if (playbackSpeedPopupMenuGroupId == menuItem.getGroupId()) { } else if (playbackSpeedPopupMenuGroupId == menuItem.getGroupId()) {
int speedIndex = menuItem.getItemId(); int speedIndex = menuItem.getItemId();
float speed = PLAYBACK_SPEEDS[speedIndex]; float speed = PLAYBACK_SPEEDS[speedIndex];
setPlaybackSpeed(speed); setPlaybackSpeed(speed);
playbackSpeed.setText(formatSpeed(speed)); playbackSpeedTextView.setText(formatSpeed(speed));
} }
return false; return false;
@ -564,6 +478,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void onDismiss(PopupMenu menu) { public void onDismiss(PopupMenu menu) {
if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
isSomePopupMenuVisible = false; isSomePopupMenuVisible = false;
qualityTextView.setText(getSelectedVideoStream().resolution);
} }
public void onQualitySelectorClicked() { public void onQualitySelectorClicked() {
@ -572,6 +487,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
isSomePopupMenuVisible = true; isSomePopupMenuVisible = true;
showControls(300); showControls(300);
VideoStream videoStream = getSelectedVideoStream();
qualityTextView.setText(MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
wasPlaying = simpleExoPlayer.getPlayWhenReady(); wasPlaying = simpleExoPlayer.getPlayWhenReady();
} }
@ -635,11 +552,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
return -1; return -1;
} }
public String resolutionStringOf(final Format format) {
final String frameRate = format.frameRate > 0 ? String.valueOf((int) format.frameRate) : "";
return Math.min(format.width, format.height) + "p" + frameRate;
}
public boolean isControlsVisible() { public boolean isControlsVisible() {
return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE; return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE;
} }
@ -746,10 +658,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
return wasPlaying; return wasPlaying;
} }
public int getQualityPopupMenuGroupId() {
return qualityPopupMenuGroupId;
}
public VideoStream getSelectedVideoStream() { public VideoStream getSelectedVideoStream() {
return availableStreams.get(selectedStreamIndex); return availableStreams.get(selectedStreamIndex);
} }

View File

@ -51,7 +51,7 @@ public final class DeferredMediaSource implements MediaSource {
* Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution * Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution
* from a given StreamInfo. * from a given StreamInfo.
* */ * */
MediaSource sourceOf(final StreamInfo info); MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info);
} }
private PlayQueueItem stream; private PlayQueueItem stream;
@ -102,8 +102,8 @@ public final class DeferredMediaSource implements MediaSource {
* called once only. * called once only.
* *
* If loading fails here, an error will be propagated out and result in an * If loading fails here, an error will be propagated out and result in an
* {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException}, which is delegated * {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException},
* to the player. * which is delegated to the player.
* */ * */
public synchronized void load() { public synchronized void load() {
if (stream == null) { if (stream == null) {
@ -117,7 +117,7 @@ public final class DeferredMediaSource implements MediaSource {
final Function<StreamInfo, MediaSource> onReceive = new Function<StreamInfo, MediaSource>() { final Function<StreamInfo, MediaSource> onReceive = new Function<StreamInfo, MediaSource>() {
@Override @Override
public MediaSource apply(StreamInfo streamInfo) throws Exception { public MediaSource apply(StreamInfo streamInfo) throws Exception {
return onStreamInfoReceived(streamInfo); return onStreamInfoReceived(stream, streamInfo);
} }
}; };
@ -142,17 +142,18 @@ public final class DeferredMediaSource implements MediaSource {
.subscribe(onSuccess, onError); .subscribe(onSuccess, onError);
} }
private MediaSource onStreamInfoReceived(final StreamInfo streamInfo) throws Exception { private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item,
@NonNull final StreamInfo info) throws Exception {
if (callback == null) { if (callback == null) {
throw new Exception("No available callback for resolving stream info."); throw new Exception("No available callback for resolving stream info.");
} }
final MediaSource mediaSource = callback.sourceOf(streamInfo); final MediaSource mediaSource = callback.sourceOf(item, info);
if (mediaSource == null) { if (mediaSource == null) {
throw new Exception("Unable to resolve source from stream info. URL: " + stream.getUrl() + throw new Exception("Unable to resolve source from stream info. URL: " + stream.getUrl() +
", audio count: " + streamInfo.audio_streams.size() + ", audio count: " + info.audio_streams.size() +
", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size()); ", video count: " + info.video_only_streams.size() + info.video_streams.size());
} }
return mediaSource; return mediaSource;

View File

@ -13,7 +13,7 @@ import org.schabi.newpipe.player.mediasource.DeferredMediaSource;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.MoveEvent; import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
import java.util.ArrayList; import java.util.ArrayList;
@ -65,8 +65,8 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public MediaSource sourceOf(StreamInfo info) { public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
return playbackListener.sourceOf(info); return playbackListener.sourceOf(item, info);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -83,8 +83,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
playQueueReactor = null; playQueueReactor = null;
syncReactor = null; syncReactor = null;
sources = null; sources = null;
playbackListener = null;
playQueue = null;
} }
/** /**
@ -130,8 +128,8 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
// Event Reactor // Event Reactor
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private Subscriber<PlayQueueMessage> getReactor() { private Subscriber<PlayQueueEvent> getReactor() {
return new Subscriber<PlayQueueMessage>() { return new Subscriber<PlayQueueEvent>() {
@Override @Override
public void onSubscribe(@NonNull Subscription d) { public void onSubscribe(@NonNull Subscription d) {
if (playQueueReactor != null) playQueueReactor.cancel(); if (playQueueReactor != null) playQueueReactor.cancel();
@ -140,7 +138,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
} }
@Override @Override
public void onNext(@NonNull PlayQueueMessage playQueueMessage) { public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
onPlayQueueChanged(playQueueMessage); onPlayQueueChanged(playQueueMessage);
} }
@ -152,7 +150,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
}; };
} }
private void onPlayQueueChanged(final PlayQueueMessage event) { private void onPlayQueueChanged(final PlayQueueEvent event) {
if (playQueue.isEmpty()) { if (playQueue.isEmpty()) {
playbackListener.shutdown(); playbackListener.shutdown();
} }
@ -160,6 +158,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
// why no pattern matching in Java =( // why no pattern matching in Java =(
switch (event.type()) { switch (event.type()) {
case INIT: case INIT:
case QUALITY:
case REORDER: case REORDER:
reset(); reset();
break; break;
@ -179,6 +178,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
move(moveEvent.getFromIndex(), moveEvent.getToIndex()); move(moveEvent.getFromIndex(), moveEvent.getToIndex());
break; break;
case ERROR: case ERROR:
case RECOVERY:
default: default:
break; break;
} }

View File

@ -43,7 +43,7 @@ public interface PlaybackListener {
* *
* May be called at any time. * May be called at any time.
* */ * */
MediaSource sourceOf(final StreamInfo info); MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info);
/** /**
* Called when the play queue can no longer to played or used. * Called when the play queue can no longer to played or used.

View File

@ -9,10 +9,12 @@ import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent; import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.InitEvent; import org.schabi.newpipe.playlist.events.InitEvent;
import org.schabi.newpipe.playlist.events.MoveEvent; import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RecoveryEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent; import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.playlist.events.SelectEvent; import org.schabi.newpipe.playlist.events.SelectEvent;
import org.schabi.newpipe.playlist.events.QualityEvent;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
@ -46,8 +48,8 @@ public abstract class PlayQueue implements Serializable {
private ArrayList<PlayQueueItem> streams; private ArrayList<PlayQueueItem> streams;
private final AtomicInteger queueIndex; private final AtomicInteger queueIndex;
private transient BehaviorSubject<PlayQueueMessage> eventBroadcast; private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueMessage> broadcastReceiver; private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient Subscription reportingReactor; private transient Subscription reportingReactor;
PlayQueue(final int index, final List<PlayQueueItem> startWith) { PlayQueue(final int index, final List<PlayQueueItem> startWith) {
@ -171,7 +173,7 @@ public abstract class PlayQueue implements Serializable {
* May be null if the play queue message bus is not initialized. * May be null if the play queue message bus is not initialized.
* */ * */
@NonNull @NonNull
public Flowable<PlayQueueMessage> getBroadcastReceiver() { public Flowable<PlayQueueEvent> getBroadcastReceiver() {
return broadcastReceiver; return broadcastReceiver;
} }
@ -273,6 +275,15 @@ public abstract class PlayQueue implements Serializable {
streams.remove(index); streams.remove(index);
} }
/**
* Moves a queue item at the source index to the target index.
*
* If the item being moved is the currently playing, then the current playing index is set
* to that of the target.
* If the moved item is not the currently playing and moves to an index <b>AFTER</b> the
* current playing index, then the current playing index is decremented.
* Vice versa if the an item after the currently playing is moved <b>BEFORE</b>.
* */
public synchronized void move(final int source, final int target) { public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return; if (source < 0 || target < 0) return;
if (source >= streams.size() || target >= streams.size()) return; if (source >= streams.size() || target >= streams.size()) return;
@ -290,6 +301,42 @@ public abstract class PlayQueue implements Serializable {
broadcast(new MoveEvent(source, target)); broadcast(new MoveEvent(source, target));
} }
/**
* Updates the quality index at the given item index.
*
* Broadcasts an update event, signalling to all recipients that they should reset.
* */
public synchronized void setQuality(final int queueIndex, final int qualityIndex) {
if (queueIndex < 0 || queueIndex >= streams.size()) return;
final PlayQueueItem item = streams.get(queueIndex);
final int oldQualityIndex = item.getQualityIndex();
item.setQualityIndex(qualityIndex);
broadcast(new QualityEvent(queueIndex, oldQualityIndex, qualityIndex));
}
/**
* Sets the recovery record of the item at the index.
*
* Broadcasts a recovery event.
* */
public synchronized void setRecovery(final int index, final long position) {
if (index < 0 || index >= streams.size()) return;
streams.get(index).setRecoveryPosition(position);
broadcast(new RecoveryEvent(index, position));
}
/**
* Revoke the recovery record of the item at the index.
*
* Broadcasts a recovery event.
* */
public synchronized void unsetRecovery(final int index) {
setRecovery(index, PlayQueueItem.RECOVERY_UNSET);
}
/** /**
* Shuffles the current play queue. * Shuffles the current play queue.
* *
@ -345,14 +392,14 @@ public abstract class PlayQueue implements Serializable {
// Rx Broadcast // Rx Broadcast
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void broadcast(final PlayQueueMessage event) { private void broadcast(final PlayQueueEvent event) {
if (eventBroadcast != null) { if (eventBroadcast != null) {
eventBroadcast.onNext(event); eventBroadcast.onNext(event);
} }
} }
private Subscriber<PlayQueueMessage> getSelfReporter() { private Subscriber<PlayQueueEvent> getSelfReporter() {
return new Subscriber<PlayQueueMessage>() { return new Subscriber<PlayQueueEvent>() {
@Override @Override
public void onSubscribe(Subscription s) { public void onSubscribe(Subscription s) {
if (reportingReactor != null) reportingReactor.cancel(); if (reportingReactor != null) reportingReactor.cancel();
@ -361,7 +408,7 @@ public abstract class PlayQueue implements Serializable {
} }
@Override @Override
public void onNext(PlayQueueMessage event) { public void onNext(PlayQueueEvent event) {
Log.d(TAG, "Received broadcast: " + event.type().name() + ". Current index: " + getIndex() + ", play queue length: " + size() + "."); Log.d(TAG, "Received broadcast: " + event.type().name() + ". Current index: " + getIndex() + ", play queue length: " + size() + ".");
reportingReactor.request(1); reportingReactor.request(1);
} }

View File

@ -10,7 +10,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.playlist.events.AppendEvent; import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent; import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.MoveEvent; import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.SelectEvent; import org.schabi.newpipe.playlist.events.SelectEvent;
@ -73,7 +73,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
} }
private void startReactor() { private void startReactor() {
final Observer<PlayQueueMessage> observer = new Observer<PlayQueueMessage>() { final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() {
@Override @Override
public void onSubscribe(@NonNull Disposable d) { public void onSubscribe(@NonNull Disposable d) {
if (playQueueReactor != null) playQueueReactor.dispose(); if (playQueueReactor != null) playQueueReactor.dispose();
@ -81,7 +81,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
} }
@Override @Override
public void onNext(@NonNull PlayQueueMessage playQueueMessage) { public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
onPlayQueueChanged(playQueueMessage); onPlayQueueChanged(playQueueMessage);
} }
@ -99,8 +99,12 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
.subscribe(observer); .subscribe(observer);
} }
private void onPlayQueueChanged(final PlayQueueMessage message) { private void onPlayQueueChanged(final PlayQueueEvent message) {
switch (message.type()) { switch (message.type()) {
case RECOVERY:
case QUALITY:
// Do nothing.
break;
case SELECT: case SELECT:
final SelectEvent selectEvent = (SelectEvent) message; final SelectEvent selectEvent = (SelectEvent) message;
notifyItemChanged(selectEvent.getOldIndex()); notifyItemChanged(selectEvent.getOldIndex());

View File

@ -36,6 +36,11 @@ public class PlayQueueItem implements Serializable {
this.stream = Single.just(info); this.stream = Single.just(info);
} }
PlayQueueItem(@NonNull final StreamInfo info, final int qualityIndex) {
this(info);
this.qualityIndex = qualityIndex;
}
PlayQueueItem(@NonNull final StreamInfoItem item) { PlayQueueItem(@NonNull final StreamInfoItem item) {
this(item.name, item.url, item.service_id, item.duration, item.thumbnail_url, item.uploader_name); this(item.name, item.url, item.service_id, item.duration, item.thumbnail_url, item.uploader_name);
} }
@ -49,8 +54,8 @@ public class PlayQueueItem implements Serializable {
this.thumbnailUrl = thumbnailUrl; this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader; this.uploader = uploader;
resetQualityIndex(); this.qualityIndex = DEFAULT_QUALITY;
resetRecoveryPosition(); this.recoveryPosition = RECOVERY_UNSET;
} }
@NonNull @NonNull
@ -71,14 +76,24 @@ public class PlayQueueItem implements Serializable {
return duration; return duration;
} }
@NonNull
public String getThumbnailUrl() { public String getThumbnailUrl() {
return thumbnailUrl; return thumbnailUrl;
} }
@NonNull
public String getUploader() { public String getUploader() {
return uploader; return uploader;
} }
public int getQualityIndex() {
return qualityIndex;
}
public long getRecoveryPosition() {
return recoveryPosition;
}
@Nullable @Nullable
public Throwable getError() { public Throwable getError() {
return error; return error;
@ -105,30 +120,14 @@ public class PlayQueueItem implements Serializable {
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Item States // Item States, keep external access out
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
public int getQualityIndex() { /*package-private*/ void setQualityIndex(final int qualityIndex) {
return qualityIndex;
}
public long getRecoveryPosition() {
return recoveryPosition;
}
public void setQualityIndex(int qualityIndex) {
this.qualityIndex = qualityIndex; this.qualityIndex = qualityIndex;
} }
public void setRecoveryPosition(long recoveryPosition) { /*package-private*/ void setRecoveryPosition(final long recoveryPosition) {
this.recoveryPosition = recoveryPosition; this.recoveryPosition = recoveryPosition;
} }
public void resetQualityIndex() {
this.qualityIndex = DEFAULT_QUALITY;
}
public void resetRecoveryPosition() {
this.recoveryPosition = RECOVERY_UNSET;
}
} }

View File

@ -9,6 +9,10 @@ public final class SinglePlayQueue extends PlayQueue {
super(0, Collections.singletonList(new PlayQueueItem(info))); super(0, Collections.singletonList(new PlayQueueItem(info)));
} }
public SinglePlayQueue(final StreamInfo info, final int qualityIndex) {
super(0, Collections.singletonList(new PlayQueueItem(info, qualityIndex)));
}
@Override @Override
public boolean isComplete() { public boolean isComplete() {
return true; return true;

View File

@ -1,12 +1,12 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public class AppendEvent implements PlayQueueMessage { public class AppendEvent implements PlayQueueEvent {
final private int amount; final private int amount;
@Override @Override
public PlayQueueEvent type() { public PlayQueueEventType type() {
return PlayQueueEvent.APPEND; return PlayQueueEventType.APPEND;
} }
public AppendEvent(final int amount) { public AppendEvent(final int amount) {

View File

@ -1,12 +1,12 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public class ErrorEvent implements PlayQueueMessage { public class ErrorEvent implements PlayQueueEvent {
final private int index; final private int index;
@Override @Override
public PlayQueueEvent type() { public PlayQueueEventType type() {
return PlayQueueEvent.ERROR; return PlayQueueEventType.ERROR;
} }
public ErrorEvent(final int index) { public ErrorEvent(final int index) {

View File

@ -1,8 +1,8 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public class InitEvent implements PlayQueueMessage { public class InitEvent implements PlayQueueEvent {
@Override @Override
public PlayQueueEvent type() { public PlayQueueEventType type() {
return PlayQueueEvent.INIT; return PlayQueueEventType.INIT;
} }
} }

View File

@ -1,12 +1,12 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public class MoveEvent implements PlayQueueMessage { public class MoveEvent implements PlayQueueEvent {
final private int fromIndex; final private int fromIndex;
final private int toIndex; final private int toIndex;
@Override @Override
public PlayQueueEvent type() { public PlayQueueEventType type() {
return PlayQueueEvent.MOVE; return PlayQueueEventType.MOVE;
} }
public MoveEvent(final int oldIndex, final int newIndex) { public MoveEvent(final int oldIndex, final int newIndex) {

View File

@ -1,24 +1,7 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public enum PlayQueueEvent { import java.io.Serializable;
INIT,
// sent when the index is changed public interface PlayQueueEvent extends Serializable {
SELECT, PlayQueueEventType type();
// sent when more streams are added to the play queue
APPEND,
// sent when a pending stream is removed from the play queue
REMOVE,
// sent when two streams swap place in the play queue
MOVE,
// sent when queue is shuffled
REORDER,
// sent when the item at index has caused an exception
ERROR
} }

View File

@ -0,0 +1,30 @@
package org.schabi.newpipe.playlist.events;
public enum PlayQueueEventType {
INIT,
// sent when the index is changed
SELECT,
// sent when more streams are added to the play queue
APPEND,
// sent when a pending stream is removed from the play queue
REMOVE,
// sent when two streams swap place in the play queue
MOVE,
// sent when queue is shuffled
REORDER,
// sent when quality index is set on a stream
QUALITY,
// sent when recovery record is set on a stream
RECOVERY,
// sent when the item at index has caused an exception
ERROR
}

View File

@ -1,7 +0,0 @@
package org.schabi.newpipe.playlist.events;
import java.io.Serializable;
public interface PlayQueueMessage extends Serializable {
PlayQueueEvent type();
}

View File

@ -0,0 +1,31 @@
package org.schabi.newpipe.playlist.events;
public class QualityEvent implements PlayQueueEvent {
final private int streamIndex;
final private int oldQualityIndex;
final private int newQualityIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.QUALITY;
}
public QualityEvent(final int streamIndex, final int oldQualityIndex, final int newQualityIndex) {
this.streamIndex = streamIndex;
this.oldQualityIndex = oldQualityIndex;
this.newQualityIndex = newQualityIndex;
}
public int getStreamIndex() {
return streamIndex;
}
public int getOldQualityIndex() {
return oldQualityIndex;
}
public int getNewQualityIndex() {
return newQualityIndex;
}
}

View File

@ -0,0 +1,25 @@
package org.schabi.newpipe.playlist.events;
public class RecoveryEvent implements PlayQueueEvent {
final private int index;
final private long position;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.RECOVERY;
}
public RecoveryEvent(final int index, final long position) {
this.index = index;
this.position = position;
}
public int getIndex() {
return index;
}
public long getPosition() {
return position;
}
}

View File

@ -1,12 +1,12 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public class RemoveEvent implements PlayQueueMessage { public class RemoveEvent implements PlayQueueEvent {
final private int index; final private int index;
@Override @Override
public PlayQueueEvent type() { public PlayQueueEventType type() {
return PlayQueueEvent.REMOVE; return PlayQueueEventType.REMOVE;
} }
public RemoveEvent(final int index) { public RemoveEvent(final int index) {

View File

@ -1,9 +1,9 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public class ReorderEvent implements PlayQueueMessage { public class ReorderEvent implements PlayQueueEvent {
@Override @Override
public PlayQueueEvent type() { public PlayQueueEventType type() {
return PlayQueueEvent.REORDER; return PlayQueueEventType.REORDER;
} }
public ReorderEvent() { public ReorderEvent() {

View File

@ -1,13 +1,13 @@
package org.schabi.newpipe.playlist.events; package org.schabi.newpipe.playlist.events;
public class SelectEvent implements PlayQueueMessage { public class SelectEvent implements PlayQueueEvent {
final private int oldIndex; final private int oldIndex;
final private int newIndex; final private int newIndex;
@Override @Override
public PlayQueueEvent type() { public PlayQueueEventType type() {
return PlayQueueEvent.SELECT; return PlayQueueEventType.SELECT;
} }
public SelectEvent(final int oldIndex, final int newIndex) { public SelectEvent(final int oldIndex, final int newIndex) {

View File

@ -152,13 +152,4 @@ public class Localization {
} }
return output; return output;
} }
public static int resolutionOf(final String resolution) {
final String[] candidates = TextUtils.split(resolution, "p");
if (candidates.length > 0 && TextUtils.isDigitsOnly(candidates[0])) {
return Integer.parseInt(candidates[0]);
} else {
return Integer.MAX_VALUE;
}
}
} }

View File

@ -60,26 +60,20 @@ public class NavigationHelper {
public static Intent getPlayerIntent(final Context context, public static Intent getPlayerIntent(final Context context,
final Class targetClazz, final Class targetClazz,
final PlayQueue playQueue, final PlayQueue playQueue,
final boolean isAppending) { final int repeatMode,
final float playbackSpeed,
final float playbackPitch) {
return getPlayerIntent(context, targetClazz, playQueue) return getPlayerIntent(context, targetClazz, playQueue)
.putExtra(BasePlayer.APPEND_ONLY, isAppending); .putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
.putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch);
} }
public static Intent getPlayerIntent(final Context context, public static Intent getPlayerEnqueueIntent(final Context context,
final Class targetClazz, final Class targetClazz,
final PlayQueue playQueue, final PlayQueue playQueue) {
final int maxResolution) {
return getPlayerIntent(context, targetClazz, playQueue) return getPlayerIntent(context, targetClazz, playQueue)
.putExtra(VideoPlayer.MAX_RESOLUTION, maxResolution); .putExtra(BasePlayer.APPEND_ONLY, true);
}
public static Intent getPlayerIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue,
final int maxResolution,
final float playbackSpeed) {
return getPlayerIntent(context, targetClazz, playQueue, maxResolution)
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,274 @@
<?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/progress_bar"
android:layout_toLeftOf="@+id/control_pane"
android:layout_toStartOf="@+id/control_pane"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/>
<RelativeLayout
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_above="@id/progress_bar"
android:layout_below="@id/appbar"
android:id="@+id/control_pane">
<LinearLayout
android:id="@+id/metadata"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/playback_controls_top"
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="3"
android:textColor="?attr/colorAccent"
android:textSize="14sp"
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="3"
android:textSize="12sp"
tools:text="Duis posuere arcu condimentum lobortis mattis." />
</LinearLayout>
<RelativeLayout
android:id="@+id/playback_controls_top"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="10dp"
android:layout_above="@+id/playback_controls_bottom"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<ImageButton
android:id="@+id/control_backward"
android:layout_width="40dp"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_marginLeft="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/exo_controls_previous"
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_centerHorizontal="true"
android:layout_centerInParent="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="#00000000"
android:padding="2dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitCenter"
android:src="@drawable/ic_pause_white"
tools:ignore="ContentDescription"/>
<ProgressBar
android:id="@+id/control_progress_bar"
style="?android:attr/progressBarStyleLargeInverse"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:layout_centerInParent="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="#00000000"
android:padding="2dp"
android:clickable="false"
android:scaleType="fitCenter"
android:indeterminate="true"
android:visibility="invisible"/>
<ImageButton
android:id="@+id/control_forward"
android:layout_width="40dp"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:layout_toRightOf="@+id/control_play_pause"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/playback_controls_bottom"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="10dp"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<TextView
android:id="@+id/control_playback_speed"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/control_repeat"
android:gravity="center"
android:minWidth="50dp"
android:text="1x"
android:textColor="@android:color/white"
android:textStyle="bold"
tools:ignore="HardcodedText,RtlHardcoded"/>
<ImageButton
android:id="@+id/control_repeat"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_toLeftOf="@+id/anchor"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_repeat_white"
tools:ignore="ContentDescription"/>
<View android:layout_width="10dp"
android:layout_height="1dp"
android:layout_centerInParent="true"
android:id="@+id/anchor"/>
<ImageButton
android:id="@+id/control_shuffle"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_toRightOf="@+id/anchor"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:background="#00000000"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_shuffle_white_24dp"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/control_playback_pitch"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/control_shuffle"
android:gravity="center"
android:minWidth="50dp"
android:text="100%"
android:textColor="@android:color/white"
android:textStyle="bold"
tools:ignore="HardcodedText,RtlHardcoded"/>
</RelativeLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:background="@drawable/player_controls_bg"
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>

View File

@ -43,26 +43,72 @@
tools:visibility="visible"/> tools:visibility="visible"/>
<RelativeLayout <RelativeLayout
android:id="@+id/play_queue_control" android:id="@+id/playQueuePanel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#64000000" android:background="#64000000"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<ImageButton <RelativeLayout
android:id="@+id/play_queue_close_area"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="60dp" android:layout_height="60dp"
android:background="?android:selectableItemBackground" android:id="@+id/playQueueControl">
<ImageButton
android:id="@+id/playQueueClose"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="40dp"
android:layout_marginEnd="40dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_close_white_24dp" android:src="@drawable/ic_close_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription"/> tools:ignore="ContentDescription"/>
<ImageButton
android:id="@+id/repeatButton"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="40dp"
android:layout_marginStart="40dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/exo_controls_repeat_off"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageButton
android:id="@+id/shuffleButton"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/repeatButton"
android:layout_marginLeft="15dp"
android:layout_marginStart="15dp"
android:clickable="true"
android:focusable="true"
android:scaleType="fitXY"
android:src="@drawable/ic_shuffle_white_24dp"
android:background="?android:selectableItemBackground"
tools:ignore="ContentDescription,RtlHardcoded"/>
</RelativeLayout>
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/play_queue" android:id="@+id/playQueue"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_below="@id/play_queue_close_area" android:layout_below="@id/playQueueControl"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager" app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/play_queue_item"/> tools:listitem="@layout/play_queue_item"/>
@ -164,7 +210,7 @@
android:layout_height="35dp" android:layout_height="35dp"
android:layout_marginLeft="2dp" android:layout_marginLeft="2dp"
android:layout_marginRight="2dp" android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/repeatButton" android:layout_toLeftOf="@+id/queueButton"
android:background="#00ffffff" android:background="#00ffffff"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
@ -173,21 +219,6 @@
android:src="@drawable/ic_screen_rotation_white" android:src="@drawable/ic_screen_rotation_white"
tools:ignore="ContentDescription,RtlHardcoded"/> tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageButton
android:id="@+id/repeatButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/queueButton"
android:background="#00ffffff"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
android:src="@drawable/exo_controls_repeat_off"
tools:ignore="ContentDescription,RtlHardcoded"/>
<ImageButton <ImageButton
android:id="@+id/queueButton" android:id="@+id/queueButton"
android:layout_width="30dp" android:layout_width="30dp"
@ -403,12 +434,4 @@
tools:visibility="visible"/> tools:visibility="visible"/>
</RelativeLayout> </RelativeLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/video_playlist"
android:layout_width="480dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#64000000"
android:visibility="gone"/>
</RelativeLayout> </RelativeLayout>

View File

@ -163,7 +163,7 @@
android:focusable="true" android:focusable="true"
android:padding="2dp" android:padding="2dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_action_av_fast_rewind" android:src="@drawable/exo_controls_previous"
tools:ignore="ContentDescription"/> tools:ignore="ContentDescription"/>
<ImageButton <ImageButton
@ -212,7 +212,7 @@
android:focusable="true" android:focusable="true"
android:padding="2dp" android:padding="2dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_action_av_fast_forward" android:src="@drawable/exo_controls_next"
tools:ignore="ContentDescription"/> tools:ignore="ContentDescription"/>
<ImageButton <ImageButton