1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-10-24 11:57:38 +00:00

- 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:
John Zhen M
2017-10-02 23:38:46 -07:00
committed by John Zhen Mo
parent bd9ee18e56
commit a9aee21e58
18 changed files with 794 additions and 249 deletions

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}
};

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);
}
}