diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3f70937b..371a20da8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,7 +40,7 @@ android:label="@string/background_player_name" /> 0) mIntent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); + Intent mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, PopupVideoPlayer.class, info, selectedStreamId); activity.startService(mIntent); } }); @@ -624,24 +598,10 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); Intent intent; - AudioStream audioStream = - info.audio_streams.get(Utils.getPreferredAudioFormat(activity, info.audio_streams)); - if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) { - //internal music player: explicit intent - if (!BackgroundPlayer.isRunning && streamThumbnail != null) { - ActivityCommunicator.getCommunicator() - .backgroundPlayerThumbnail = streamThumbnail; - intent = new Intent(activity, BackgroundPlayer.class); - - intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(audioStream.url), - MediaFormat.getMimeById(audioStream.format)); - intent.putExtra(BackgroundPlayer.TITLE, info.title); - intent.putExtra(BackgroundPlayer.WEB_URL, info.webpage_url); - intent.putExtra(BackgroundPlayer.SERVICE_ID, serviceId); - intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader); - activity.startService(intent); - } + AudioStream audioStream = info.audio_streams.get(Utils.getPreferredAudioFormat(activity, info.audio_streams)); + if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { + activity.startService(NavigationHelper.getOpenBackgroundPlayerIntent(activity, info, audioStream)); + Toast.makeText(activity, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); } else { intent = new Intent(); try { @@ -829,9 +789,7 @@ public class VideoDetailFragment extends Fragment implements StreamExtractorWork || (Build.VERSION.SDK_INT < 16); if (!useOldPlayer) { // ExoPlayer - if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - mIntent = NavigationHelper.getOpenPlayerIntent(activity, ExoPlayerActivity.class, info, actionBarHandler.getSelectedVideoStream()); - if (info.start_position > 0) mIntent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); + mIntent = NavigationHelper.getOpenVideoPlayerIntent(activity, MainVideoPlayer.class, info, actionBarHandler.getSelectedVideoStream()); } else { // Internal Player mIntent = new Intent(activity, PlayVideoActivity.class) diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 3c6bbad81..8c9c80a3d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -4,584 +4,452 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.media.AudioManager; -import android.media.MediaPlayer; import android.net.wifi.WifiManager; +import android.os.Build; import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; import android.os.PowerManager; -import android.support.v7.app.NotificationCompat; +import android.support.annotation.IntRange; +import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.RemoteViews; -import android.widget.Toast; -import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.stream_info.AudioStream; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ThemeHelper; + +import java.io.Serializable; -import java.io.IOException; -import java.util.Arrays; /** - * Created by Adam Howard on 08/11/15. - * Copyright (c) Adam Howard 2015 + * Base players joining the common properties * - * BackgroundPlayer.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . + * @author mauriciocolli */ - -/**Plays the audio stream of videos in the background.*/ -public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPreparedListener*/ { - +public class BackgroundPlayer extends Service { private static final String TAG = "BackgroundPlayer"; - private static final String CLASSNAME = "org.schabi.newpipe.player.BackgroundPlayer"; - private static final String ACTION_STOP = CLASSNAME + ".STOP"; - private static final String ACTION_PLAYPAUSE = CLASSNAME + ".PLAYPAUSE"; - private static final String ACTION_REWIND = CLASSNAME + ".REWIND"; - private static final String ACTION_PLAYBACK_STATE = CLASSNAME + ".PLAYBACK_STATE"; - private static final String EXTRA_PLAYBACK_STATE = CLASSNAME + ".extras.EXTRA_PLAYBACK_STATE"; + private static final boolean DEBUG = BasePlayer.DEBUG; - // Extra intent arguments - public static final String TITLE = "title"; - public static final String WEB_URL = "web_url"; - public static final String SERVICE_ID = "service_id"; - public static final String CHANNEL_NAME = "channel_name"; + public static final String ACTION_CLOSE = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE"; + public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE"; + public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.BackgroundPlayer.OPEN_DETAIL"; + public static final String ACTION_REPEAT = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT"; + public static final String ACTION_FAST_REWIND = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND"; + public static final String ACTION_FAST_FORWARD = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD"; - private volatile String webUrl = ""; - private volatile int serviceId = -1; - private volatile String channelName = ""; + public static final String AUDIO_STREAM = "video_only_audio_stream"; + private AudioStream audioStream; - // Determines if the service is already running. - // Prevents launching the service twice. - public static volatile boolean isRunning; + private BasePlayerImpl basePlayerImpl; + private PowerManager powerManager; + private WifiManager wifiManager; - public BackgroundPlayer() { - super(); - } + private PowerManager.WakeLock wakeLock; + private WifiManager.WifiLock wifiLock; + + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ + private static final int NOTIFICATION_ID = 123789; + + private NotificationManager notificationManager; + private NotificationCompat.Builder notBuilder; + private RemoteViews notRemoteView; + private RemoteViews bigNotRemoteView; + private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; + + + /*////////////////////////////////////////////////////////////////////////// + // Service's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreate() { - /*PendingIntent pi = PendingIntent.getActivity(this, 0, - new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);*/ - super.onCreate(); + if (DEBUG) Log.d(TAG, "onCreate() called"); + notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); + powerManager = ((PowerManager) getSystemService(POWER_SERVICE)); + wifiManager = ((WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE)); + + ThemeHelper.setTheme(this, false); + basePlayerImpl = new BasePlayerImpl(this); + basePlayerImpl.setup(); } + @Override public int onStartCommand(Intent intent, int flags, int startId) { - Toast.makeText(this, R.string.background_player_playing_toast, - Toast.LENGTH_SHORT).show(); - - String source = intent.getDataString(); - //Log.i(TAG, "backgroundPLayer source:"+source); - String videoTitle = intent.getStringExtra(TITLE); - webUrl = intent.getStringExtra(WEB_URL); - serviceId = intent.getIntExtra(SERVICE_ID, -1); - channelName = intent.getStringExtra(CHANNEL_NAME); - - //do nearly everything in a separate thread - PlayerThread player = new PlayerThread(source, videoTitle, this); - player.start(); - - isRunning = true; - - // If we get killed after returning here, don't restart + if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); + basePlayerImpl.handleIntent(intent); return START_NOT_STICKY; } @Override - public IBinder onBind(Intent intent) { - // We don't provide binding (yet?), so return null - return null; + public void onDestroy() { + if (DEBUG) Log.d(TAG, "destroy() called"); + releaseWifiAndCpu(); + stopForeground(true); + if (basePlayerImpl != null) basePlayerImpl.destroy(); } @Override - public void onDestroy() { - //Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show(); - isRunning = false; + public IBinder onBind(Intent intent) { + return null; } + /*////////////////////////////////////////////////////////////////////////// + // Actions + //////////////////////////////////////////////////////////////////////////*/ - private class PlayerThread extends Thread { - MediaPlayer mediaPlayer; - private String source; - private String title; - private int noteID = TAG.hashCode(); - private BackgroundPlayer owner; - private NotificationManager noteMgr; - private WifiManager.WifiLock wifiLock; - private Bitmap videoThumbnail; - private NoteBuilder noteBuilder; - private volatile boolean donePlaying = false; + 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); + i.putExtra(Constants.KEY_SERVICE_ID, 0); + i.putExtra(Constants.KEY_URL, videoUrl); + i.putExtra(Constants.KEY_TITLE, videoTitle); + i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } - public PlayerThread(String src, String title, BackgroundPlayer owner) { - this.source = src; - this.title = title; - this.owner = owner; - mediaPlayer = new MediaPlayer(); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - } + private void onClose() { + if (basePlayerImpl != null) basePlayerImpl.destroyPlayer(); + stopForeground(true); + releaseWifiAndCpu(); + stopSelf(); + } - public boolean isDonePlaying() { - return donePlaying; - } + private void onScreenOnOff(boolean on) { + if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); + if (on) { + if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning.get()) basePlayerImpl.startProgressLoop(); + } else basePlayerImpl.stopProgressLoop(); - private boolean isPlaying() { - try { - return mediaPlayer.isPlaying(); - } catch (IllegalStateException e) { - Log.w(TAG, "Unable to retrieve playing state", e); - return false; - } - } + } - private void setDonePlaying() { - donePlaying = true; - synchronized (PlayerThread.this) { - PlayerThread.this.notifyAll(); - } - } + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ - private synchronized PlaybackState getPlaybackState() { - try { - return new PlaybackState(mediaPlayer.getDuration(), mediaPlayer.getCurrentPosition(), isPlaying()); - } catch (IllegalStateException e) { - // This isn't that nice way to handle this. - // maybe there is a better way - Log.w(TAG, this + ": Got illegal state exception while creating playback state", e); - return PlaybackState.UNPREPARED; - } - } + private NotificationCompat.Builder createNotification() { + notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); + bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); - private void broadcastState() { - PlaybackState state = getPlaybackState(); - if(state == null) return; - Intent intent = new Intent(ACTION_PLAYBACK_STATE); - intent.putExtra(EXTRA_PLAYBACK_STATE, state); - sendBroadcast(intent); - } + setupNotification(notRemoteView); + setupNotification(bigNotRemoteView); - @Override - public void run() { - mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);//cpu lock - try { - mediaPlayer.setDataSource(source); - //We are already in a separate worker thread, - //so calling the blocking prepare() method should be ok - mediaPlayer.prepare(); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCustomContentView(notRemoteView) + .setCustomBigContentView(bigNotRemoteView); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(Notification.PRIORITY_MAX); + return builder; + } - } catch (IOException ioe) { - ioe.printStackTrace(); - Log.e(TAG, "video source:" + source); - Log.e(TAG, "video title:" + title); - //can't do anything useful without a file to play; exit early - return; - } + private void setupNotification(RemoteViews remoteViews) { + //if (videoThumbnail != null) remoteViews.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + ///else remoteViews.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); + remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle()); + remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getChannelName()); - try { - videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; - } catch (Exception e) { - Log.e(TAG, "Could not get video thumbnail from ActivityCommunicator"); - e.printStackTrace(); - } + remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationStop, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationContent, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); - WifiManager wifiMgr = (WifiManager)getSystemService(Context.WIFI_SERVICE); - wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); - - //listen for end of video - mediaPlayer.setOnCompletionListener(new EndListener(wifiLock)); - - //get audio focus - /* - AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); - - if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - // could not get audio focus. - }*/ - wifiLock.acquire(); - mediaPlayer.start(); - - IntentFilter filter = new IntentFilter(); - filter.setPriority(Integer.MAX_VALUE); - filter.addAction(ACTION_PLAYPAUSE); - filter.addAction(ACTION_STOP); - filter.addAction(ACTION_REWIND); - filter.addAction(ACTION_PLAYBACK_STATE); - registerReceiver(broadcastReceiver, filter); - - initNotificationBuilder(); - startForeground(noteID, noteBuilder.build()); - - //currently decommissioned progressbar looping update code - works, but doesn't fit inside - //Notification.MediaStyle Notification layout. - noteMgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); - - //update every 2s or 4 times in the video, whichever is shorter - int vidLength = mediaPlayer.getDuration(); - int sleepTime = Math.min(2000, (int)(vidLength / 4)); - while(!isDonePlaying()) { - broadcastState(); - try { - synchronized (this) { - wait(sleepTime); - } - } catch (InterruptedException e) { - Log.e(TAG, "sleep failure", e); - } - } - } - - /**Handles button presses from the notification. */ - private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - //Log.i(TAG, "received broadcast action:"+action); - switch (action) { - case ACTION_PLAYPAUSE: { - boolean isPlaying = mediaPlayer.isPlaying(); - if(isPlaying) { - mediaPlayer.pause(); - } else { - //reacquire CPU lock after auto-releasing it on pause - mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); - mediaPlayer.start(); - } - synchronized (PlayerThread.this) { - PlayerThread.this.notifyAll(); - } - break; - } - case ACTION_REWIND: - mediaPlayer.seekTo(0); - synchronized (PlayerThread.this) { - PlayerThread.this.notifyAll(); - } - break; - case ACTION_STOP: - //this auto-releases CPU lock - mediaPlayer.stop(); - afterPlayCleanup(); - break; - case ACTION_PLAYBACK_STATE: { - PlaybackState playbackState = intent.getParcelableExtra(EXTRA_PLAYBACK_STATE); - if(!playbackState.equals(PlaybackState.UNPREPARED)) { - noteBuilder.setProgress(playbackState.getDuration(), playbackState.getPlayedTime(), false); - noteBuilder.setIsPlaying(playbackState.isPlaying()); - } else { - noteBuilder.setProgress(0, 0, true); - } - noteMgr.notify(noteID, noteBuilder.build()); - break; - } - } - } - }; - - private void afterPlayCleanup() { - // Notify thread to stop - setDonePlaying(); - //remove progress bar - //noteBuilder.setProgress(0, 0, false); - - //remove notification - noteMgr.cancel(noteID); - unregisterReceiver(broadcastReceiver); - //release mediaPlayer's system resources - mediaPlayer.release(); - - //release wifilock - wifiLock.release(); - //remove foreground status of service; make BackgroundPlayer killable - stopForeground(true); - try { - // Wait for thread to stop - PlayerThread.this.join(); - } catch (InterruptedException e) { - Log.e(TAG, "unable to join player thread", e); - } - stopSelf(); - } - - private class EndListener implements MediaPlayer.OnCompletionListener { - private WifiManager.WifiLock wl; - public EndListener(WifiManager.WifiLock wifiLock) { - this.wl = wifiLock; - } - - @Override - public void onCompletion(MediaPlayer mp) { - afterPlayCleanup(); - } - } - - private void initNotificationBuilder() { - Notification note; - Resources res = getApplicationContext().getResources(); - - /* - NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder - (R.drawable.ic_pause_white, "Pause", playPI).build(); - */ - - PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID, - new Intent(ACTION_PLAYPAUSE), PendingIntent.FLAG_UPDATE_CURRENT); - PendingIntent stopPI = PendingIntent.getBroadcast(owner, noteID, - new Intent(ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT); - PendingIntent rewindPI = PendingIntent.getBroadcast(owner, noteID, - new Intent(ACTION_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); - - //build intent to return to video, on tapping notification - Intent openDetailViewIntent = new Intent(getApplicationContext(), MainActivity.class); - openDetailViewIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); - openDetailViewIntent.putExtra(Constants.KEY_URL, webUrl); - openDetailViewIntent.putExtra(Constants.KEY_TITLE, title); - openDetailViewIntent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); - openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID, - openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT); - noteBuilder = new NoteBuilder(owner, playPI, stopPI, rewindPI, openDetailView); - noteBuilder - .setTitle(title) - .setArtist(channelName) - .setOngoing(true) - .setDeleteIntent(stopPI) - //doesn't fit with Notification.MediaStyle - //.setProgress(vidLength, 0, false) - .setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp) - .setContentIntent(PendingIntent.getActivity(getApplicationContext(), - noteID, openDetailViewIntent, - PendingIntent.FLAG_UPDATE_CURRENT)) - .setContentIntent(openDetailView) - .setCategory(Notification.CATEGORY_TRANSPORT) - //Make notification appear on lockscreen - .setVisibility(Notification.VISIBILITY_PUBLIC); - } - - - /** - * Notification builder which works like the real builder but uses a custom view. - */ - class NoteBuilder extends NotificationCompat.Builder { - - /** - * @param context - * @inheritDoc - */ - public NoteBuilder(Context context, PendingIntent playPI, PendingIntent stopPI, - PendingIntent rewindPI, PendingIntent openDetailView) { - super(context); - setCustomContentView(createCustomContentView(playPI, stopPI, rewindPI, openDetailView)); - setCustomBigContentView(createCustomBigContentView(playPI, stopPI, rewindPI, openDetailView)); - } - - private RemoteViews createCustomBigContentView(PendingIntent playPI, - PendingIntent stopPI, - PendingIntent rewindPI, - PendingIntent openDetailView) { - //possibly found the expandedView problem, - //but can't test it as I don't have a 5.0 device. -medavox - RemoteViews expandedView = - new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); - expandedView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); - expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI); - expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); - expandedView.setOnClickPendingIntent(R.id.notificationRewind, rewindPI); - expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView); - return expandedView; - } - - private RemoteViews createCustomContentView(PendingIntent playPI, PendingIntent stopPI, - PendingIntent rewindPI, - PendingIntent openDetailView) { - RemoteViews view = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); - view.setImageViewBitmap(R.id.notificationCover, videoThumbnail); - view.setOnClickPendingIntent(R.id.notificationStop, stopPI); - view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI); - view.setOnClickPendingIntent(R.id.notificationRewind, rewindPI); - view.setOnClickPendingIntent(R.id.notificationContent, openDetailView); - return view; - } - - /** - * Set the title of the stream - * @param title the title of the stream - * @return this builder for chaining - */ - NoteBuilder setTitle(String title) { - setContentTitle(title); - getContentView().setTextViewText(R.id.notificationSongName, title); - getBigContentView().setTextViewText(R.id.notificationSongName, title); - setTicker(String.format(getBaseContext().getString( - R.string.background_player_time_text), title)); - return this; - } - - /** - * Set the artist of the stream - * @param artist the artist of the stream - * @return this builder for chaining - */ - NoteBuilder setArtist(String artist) { - setSubText(artist); - getContentView().setTextViewText(R.id.notificationArtist, artist); - getBigContentView().setTextViewText(R.id.notificationArtist, artist); - return this; - } - - @Override - public android.support.v4.app.NotificationCompat.Builder setProgress(int max, int progress, boolean indeterminate) { - super.setProgress(max, progress, indeterminate); - getBigContentView().setProgressBar(R.id.playbackProgress, max, progress, indeterminate); - return this; - } - - /** - * Set the isPlaying state - * @param isPlaying the is playing state - */ - public void setIsPlaying(boolean isPlaying) { - RemoteViews views = getContentView(), bigViews = getBigContentView(); - int imageSrc; - if(isPlaying) { - imageSrc = R.drawable.ic_pause_white; - } else { - imageSrc = R.drawable.ic_play_circle_filled_white_24dp; - } - views.setImageViewResource(R.id.notificationPlayPause, imageSrc); - bigViews.setImageViewResource(R.id.notificationPlayPause, imageSrc); - - } + remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationFForward, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); + switch (basePlayerImpl.getCurrentRepeatMode()) { + case REPEAT_DISABLED: + remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 77); + break; + case REPEAT_ONE: + remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); + break; + case REPEAT_ALL: + // Waiting :) + break; } } /** - * Represents the state of the player. + * Updates the notification, and the play/pause button in it. + * Used for changes on the remoteView + * + * @param drawableId if != -1, sets the drawable with that id on the play/pause button */ - public static class PlaybackState implements Parcelable { - - private static final int INDEX_IS_PLAYING = 0; - private static final int INDEX_IS_PREPARED= 1; - private static final int INDEX_HAS_ERROR = 2; - private final int duration; - private final int played; - private final boolean[] booleanValues = new boolean[3]; - - static final PlaybackState UNPREPARED = new PlaybackState(false, false, false); - static final PlaybackState FAILED = new PlaybackState(false, false, true); - - - PlaybackState(Parcel in) { - duration = in.readInt(); - played = in.readInt(); - in.readBooleanArray(booleanValues); + private void updateNotification(int drawableId) { + if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); + 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); } + notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); + } - PlaybackState(int duration, int played, boolean isPlaying) { - this.played = played; - this.duration = duration; - this.booleanValues[INDEX_IS_PLAYING] = isPlaying; - this.booleanValues[INDEX_IS_PREPARED] = true; - this.booleanValues[INDEX_HAS_ERROR] = false; - } + private void setControlsOpacity(@IntRange(from = 0, to = 255) int opacity) { + if (notRemoteView != null) notRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); + if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); + if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); + if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); + if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); + if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); + } - private PlaybackState(boolean isPlaying, boolean isPrepared, boolean hasErrors) { - this.played = 0; - this.duration = 0; - this.booleanValues[INDEX_IS_PLAYING] = isPlaying; - this.booleanValues[INDEX_IS_PREPARED] = isPrepared; - this.booleanValues[INDEX_HAS_ERROR] = hasErrors; - } + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ - int getDuration() { - return duration; - } + private void lockWifiAndCpu() { + if (DEBUG) Log.d(TAG, "lockWifiAndCpu() called"); + if (wakeLock != null && wakeLock.isHeld() && wifiLock != null && wifiLock.isHeld()) return; - int getPlayedTime() { - return played; - } + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); - boolean isPlaying() { - return booleanValues[INDEX_IS_PLAYING]; - } + if (wakeLock != null) wakeLock.acquire(); + if (wifiLock != null) wifiLock.acquire(); + } - boolean isPrepared() { - return booleanValues[INDEX_IS_PREPARED]; - } + private void releaseWifiAndCpu() { + if (DEBUG) Log.d(TAG, "releaseWifiAndCpu() called"); + if (wakeLock != null && wakeLock.isHeld()) wakeLock.release(); + if (wifiLock != null && wifiLock.isHeld()) wifiLock.release(); - boolean hasErrors() { - return booleanValues[INDEX_HAS_ERROR]; - } + wakeLock = null; + wifiLock = null; + } + ////////////////////////////////////////////////////////////////////////// - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(duration); - dest.writeInt(played); - dest.writeBooleanArray(booleanValues); + private class BasePlayerImpl extends BasePlayer { + + BasePlayerImpl(Context context) { + super(context); } @Override - public int describeContents() { - return 0; + public void handleIntent(Intent intent) { + super.handleIntent(intent); + Serializable serializable = intent.getSerializableExtra(BackgroundPlayer.AUDIO_STREAM); + if (serializable instanceof AudioStream) audioStream = (AudioStream) serializable; + playUrl(audioStream.url, MediaFormat.getSuffixById(audioStream.format), true); + + if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); + if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); } - public static final Creator CREATOR = new Creator() { - @Override - public PlaybackState createFromParcel(Parcel in) { - return new PlaybackState(in); + @Override + public void initThumbnail() { + if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); + if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); + updateNotification(-1); + super.initThumbnail(); + } + + @Override + public void onThumbnailReceived(Bitmap thumbnail) { + super.onThumbnailReceived(thumbnail); + if (thumbnail != null) { + if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); + if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); + updateNotification(-1); } - - @Override - public PlaybackState[] newArray(int size) { - return new PlaybackState[size]; - } - }; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PlaybackState that = (PlaybackState) o; - - if (duration != that.duration) return false; - if (played != that.played) return false; - return Arrays.equals(booleanValues, that.booleanValues); - } @Override - public int hashCode() { - if(this == UNPREPARED) return 1; - if(this == FAILED) return 2; - int result = duration; - result = 31 * result + played; - result = 31 * result + Arrays.hashCode(booleanValues); - return result + 2; + public void playUrl(String url, String format, boolean autoPlay) { + super.playUrl(url, format, autoPlay); + + notBuilder = createNotification(); + startForeground(NOTIFICATION_ID, notBuilder.build()); + } + + @Override + public void onPrepared(boolean playWhenReady) { + super.onPrepared(playWhenReady); + if (simpleExoPlayer.getDuration() < 15000) { + PROGRESS_LOOP_INTERVAL = 1000; + FAST_FORWARD_REWIND_AMOUNT = 2000; + } else if (simpleExoPlayer.getDuration() > 60 * 60 * 1000) { + PROGRESS_LOOP_INTERVAL = 2000; + FAST_FORWARD_REWIND_AMOUNT = 60000; + } else { + PROGRESS_LOOP_INTERVAL = 2000; + FAST_FORWARD_REWIND_AMOUNT = 10000; + } + basePlayerImpl.getPlayer().setVolume(1f); + } + + @Override + public void onRepeatClicked() { + super.onRepeatClicked(); + + int opacity = 255; + switch (currentRepeatMode) { + case REPEAT_DISABLED: + opacity = 77; + break; + case REPEAT_ONE: + opacity = 255; + break; + case REPEAT_ALL: + // Waiting :) + break; + } + if (notRemoteView != null) notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity); + if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity); + updateNotification(-1); + } + + @Override + public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { + if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); + if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); + updateNotification(-1); + } + + @Override + public void onFastRewind() { + super.onFastRewind(); + triggerProgressUpdate(); + } + + @Override + public void onFastForward() { + super.onFastForward(); + triggerProgressUpdate(); + } + + @Override + public void onLoadingChanged(boolean isLoading) { + // Disable default behavior + } + + @Override + public void destroy() { + super.destroy(); + if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null); + if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null); + } + + @Override + public void onError(Exception exception) { + exception.printStackTrace(); + stopSelf(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast Receiver + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void setupBroadcastReceiver(IntentFilter intentFilter) { + super.setupBroadcastReceiver(intentFilter); + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_OPEN_DETAIL); + intentFilter.addAction(ACTION_REPEAT); + intentFilter.addAction(ACTION_FAST_FORWARD); + intentFilter.addAction(ACTION_FAST_REWIND); + + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + + intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); + } + + @Override + public void onBroadcastReceived(Intent intent) { + super.onBroadcastReceived(intent); + if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + switch (intent.getAction()) { + case ACTION_CLOSE: + onClose(); + break; + case ACTION_PLAY_PAUSE: + onVideoPlayPause(); + break; + case ACTION_OPEN_DETAIL: + onOpenDetail(BackgroundPlayer.this, basePlayerImpl.getVideoUrl(), basePlayerImpl.getVideoTitle()); + break; + case ACTION_REPEAT: + onRepeatClicked(); + break; + case ACTION_FAST_REWIND: + onFastRewind(); + break; + case ACTION_FAST_FORWARD: + onFastForward(); + break; + case Intent.ACTION_SCREEN_ON: + onScreenOnOff(true); + break; + case Intent.ACTION_SCREEN_OFF: + onScreenOnOff(false); + break; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onLoading() { + super.onLoading(); + + setControlsOpacity(77); + updateNotification(-1); + } + + @Override + public void onPlaying() { + super.onPlaying(); + + setControlsOpacity(255); + updateNotification(R.drawable.ic_pause_white); + + lockWifiAndCpu(); + } + + @Override + public void onPaused() { + super.onPaused(); + + updateNotification(R.drawable.ic_play_arrow_white); + if (isProgressLoopRunning.get()) stopProgressLoop(); + + releaseWifiAndCpu(); + } + + @Override + public void onCompleted() { + super.onCompleted(); + + setControlsOpacity(255); + if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); + if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); + updateNotification(R.drawable.ic_replay_white); + + releaseWifiAndCpu(); } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java new file mode 100644 index 000000000..6a85d96f4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -0,0 +1,717 @@ +package org.schabi.newpipe.player; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; +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.listener.SimpleImageLoadingListener; + +import java.io.File; +import java.util.Formatter; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Base for the players, joining the common properties + * + * @author mauriciocolli + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManager.OnAudioFocusChangeListener { + public static final boolean DEBUG = false; + public static final String TAG = "BasePlayer"; + + protected Context context; + protected SharedPreferences sharedPreferences; + protected AudioManager audioManager; + + protected BroadcastReceiver broadcastReceiver; + protected IntentFilter intentFilter; + + /*////////////////////////////////////////////////////////////////////////// + // Intent + //////////////////////////////////////////////////////////////////////////*/ + + public static final String VIDEO_URL = "video_url"; + public static final String VIDEO_TITLE = "video_title"; + public static final String VIDEO_THUMBNAIL_URL = "video_thumbnail_url"; + public static final String START_POSITION = "start_position"; + public static final String CHANNEL_NAME = "channel_name"; + + protected Bitmap videoThumbnail = null; + protected String videoUrl = ""; + protected String videoTitle = ""; + protected String videoThumbnailUrl = ""; + protected int videoStartPos = -1; + protected String channelName = ""; + + /*////////////////////////////////////////////////////////////////////////// + // Player + //////////////////////////////////////////////////////////////////////////*/ + + public int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds + public static final String CACHE_FOLDER_NAME = "exoplayer"; + + protected SimpleExoPlayer simpleExoPlayer; + protected boolean isPrepared = false; + + protected MediaSource mediaSource; + protected CacheDataSourceFactory cacheDataSourceFactory; + protected final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + protected final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + + protected int PROGRESS_LOOP_INTERVAL = 100; + protected AtomicBoolean isProgressLoopRunning = new AtomicBoolean(); + protected Handler progressLoop; + protected Runnable progressUpdate; + + //////////////////////////////////////////////////////////////////////////*/ + + public BasePlayer(Context context) { + this.context = context; + this.progressLoop = new Handler(); + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); + + this.broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + onBroadcastReceived(intent); + } + }; + this.intentFilter = new IntentFilter(); + setupBroadcastReceiver(intentFilter); + context.registerReceiver(broadcastReceiver, intentFilter); + } + + public void setup() { + if (simpleExoPlayer == null) initPlayer(); + initListeners(); + } + + private void initExoPlayerCache() { + if (cacheDataSourceFactory == null) { + DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getPackageName()), bandwidthMeter); + File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); + if (!cacheDir.exists()) { + //noinspection ResultOfMethodCallIgnored + cacheDir.mkdir(); + } + + if (DEBUG) Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath()); + SimpleCache simpleCache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(64 * 1024 * 1024L)); + cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory, CacheDataSource.FLAG_BLOCK_ON_CACHE, 512 * 1024); + } + } + + public void initPlayer() { + if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); + initExoPlayerCache(); + + AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); + DefaultTrackSelector defaultTrackSelector = new DefaultTrackSelector(trackSelectionFactory); + DefaultLoadControl loadControl = new DefaultLoadControl(); + + simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, defaultTrackSelector, loadControl); + simpleExoPlayer.addListener(this); + } + + public void initListeners() { + progressUpdate = new Runnable() { + @Override + public void run() { + //if(DEBUG) Log.d(TAG, "progressUpdate run() called"); + onUpdateProgress((int) simpleExoPlayer.getCurrentPosition(), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage()); + if (isProgressLoopRunning.get()) progressLoop.postDelayed(this, PROGRESS_LOOP_INTERVAL); + } + }; + } + + public void handleIntent(Intent intent) { + if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + if (intent == null) return; + + videoUrl = intent.getStringExtra(VIDEO_URL); + videoTitle = intent.getStringExtra(VIDEO_TITLE); + videoThumbnailUrl = intent.getStringExtra(VIDEO_THUMBNAIL_URL); + videoStartPos = intent.getIntExtra(START_POSITION, -1); + channelName = intent.getStringExtra(CHANNEL_NAME); + + initThumbnail(); + //play(getSelectedVideoStream(), true); + } + + public void initThumbnail() { + if (DEBUG) Log.d(TAG, "initThumbnail() called"); + videoThumbnail = null; + if (videoThumbnailUrl == null || videoThumbnailUrl.isEmpty()) return; + ImageLoader.getInstance().resume(); + ImageLoader.getInstance().loadImage(videoThumbnailUrl, new SimpleImageLoadingListener() { + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + if (DEBUG) Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]"); + videoThumbnail = loadedImage; + onThumbnailReceived(loadedImage); + } + }); + } + + public void playUrl(String url, String format, boolean autoPlay) { + if (DEBUG) { + Log.d(TAG, "play() called with: url = [" + url + "], autoPlay = [" + autoPlay + "]"); + } + + if (url == null || simpleExoPlayer == null) { + RuntimeException runtimeException = new RuntimeException((url == null ? "Url " : "Player ") + " null"); + onError(runtimeException); + throw runtimeException; + } + + changeState(STATE_LOADING); + + isPrepared = false; + mediaSource = buildMediaSource(url, format); + + if (simpleExoPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) simpleExoPlayer.stop(); + if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); + simpleExoPlayer.prepare(mediaSource); + simpleExoPlayer.setPlayWhenReady(autoPlay); + } + + public void destroyPlayer() { + if (DEBUG) Log.d(TAG, "destroyPlayer() called"); + if (simpleExoPlayer != null) { + simpleExoPlayer.stop(); + simpleExoPlayer.release(); + } + if (progressLoop != null && isProgressLoopRunning.get()) stopProgressLoop(); + } + + public void destroy() { + if (DEBUG) Log.d(TAG, "destroy() called"); + destroyPlayer(); + unregisterBroadcastReceiver(); + videoThumbnail = null; + simpleExoPlayer = null; + } + + public MediaSource buildMediaSource(String url, String overrideExtension) { + if (DEBUG) { + Log.d(TAG, "buildMediaSource() called with: url = [" + url + "], overrideExtension = [" + overrideExtension + "]"); + } + Uri uri = Uri.parse(url); + int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + MediaSource mediaSource; + switch (type) { + case C.TYPE_SS: + mediaSource = new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null); + break; + case C.TYPE_DASH: + mediaSource = new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null); + break; + case C.TYPE_HLS: + mediaSource = new HlsMediaSource(uri, cacheDataSourceFactory, null, null); + break; + case C.TYPE_OTHER: + mediaSource = new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null); + break; + default: { + throw new IllegalStateException("Unsupported type: " + type); + } + } + return mediaSource; + } + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast Receiver + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Add your action in the intentFilter + * + * @param intentFilter intent filter that will be used for register the receiver + */ + protected void setupBroadcastReceiver(IntentFilter intentFilter) { + intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + } + + public void onBroadcastReceived(Intent intent) { + switch (intent.getAction()) { + case AudioManager.ACTION_AUDIO_BECOMING_NOISY: + if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); + break; + } + } + + public void unregisterBroadcastReceiver() { + if (broadcastReceiver != null && context != null) { + context.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // AudioFocus + //////////////////////////////////////////////////////////////////////////*/ + + private static final int DUCK_DURATION = 1500; + private static final float DUCK_AUDIO_TO = .2f; + + @Override + public void onAudioFocusChange(int focusChange) { + if (DEBUG) Log.d(TAG, "onAudioFocusChange() called with: focusChange = [" + focusChange + "]"); + if (simpleExoPlayer == null) return; + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + onAudioFocusGain(); + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + onAudioFocusLossCanDuck(); + break; + case AudioManager.AUDIOFOCUS_LOSS: + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + onAudioFocusLoss(); + break; + } + } + + protected void onAudioFocusGain() { + if (DEBUG) Log.d(TAG, "onAudioFocusGain() called"); + if (simpleExoPlayer != null) simpleExoPlayer.setVolume(DUCK_AUDIO_TO); + animateAudio(DUCK_AUDIO_TO, 1f, DUCK_DURATION); + simpleExoPlayer.setPlayWhenReady(true); + } + + protected void onAudioFocusLoss() { + if (DEBUG) Log.d(TAG, "onAudioFocusLoss() called"); + simpleExoPlayer.setPlayWhenReady(false); + } + + protected void onAudioFocusLossCanDuck() { + if (DEBUG) Log.d(TAG, "onAudioFocusLossCanDuck() called"); + // Set the volume to 1/10 on ducking + animateAudio(simpleExoPlayer.getVolume(), DUCK_AUDIO_TO, DUCK_DURATION); + } + + /*////////////////////////////////////////////////////////////////////////// + // States Implementation + //////////////////////////////////////////////////////////////////////////*/ + + public static final int STATE_LOADING = 123; + public static final int STATE_PLAYING = 124; + public static final int STATE_BUFFERING = 125; + public static final int STATE_PAUSED = 126; + public static final int STATE_PAUSED_SEEK = 127; + public static final int STATE_COMPLETED = 128; + + + protected int currentState = -1; + + public void changeState(int state) { + if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); + currentState = state; + switch (state) { + case STATE_LOADING: + onLoading(); + break; + case STATE_PLAYING: + onPlaying(); + break; + case STATE_BUFFERING: + onBuffering(); + break; + case STATE_PAUSED: + onPaused(); + break; + case STATE_PAUSED_SEEK: + onPausedSeek(); + break; + case STATE_COMPLETED: + onCompleted(); + break; + } + } + + public void onLoading() { + if (DEBUG) Log.d(TAG, "onLoading() called"); + if (!isProgressLoopRunning.get()) startProgressLoop(); + } + + public void onPlaying() { + if (DEBUG) Log.d(TAG, "onPlaying() called"); + if (!isProgressLoopRunning.get()) startProgressLoop(); + } + + public void onBuffering() { + } + + public void onPaused() { + if (isProgressLoopRunning.get()) stopProgressLoop(); + } + + public void onPausedSeek() { + } + + public void onCompleted() { + if (DEBUG) Log.d(TAG, "onCompleted() called"); + if (isProgressLoopRunning.get()) stopProgressLoop(); + + if (currentRepeatMode == RepeatMode.REPEAT_ONE) { + changeState(STATE_LOADING); + simpleExoPlayer.seekTo(0); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Repeat + //////////////////////////////////////////////////////////////////////////*/ + + protected RepeatMode currentRepeatMode = RepeatMode.REPEAT_DISABLED; + + public enum RepeatMode { + REPEAT_DISABLED, + REPEAT_ONE, + REPEAT_ALL + } + + public void onRepeatClicked() { + if (DEBUG) Log.d(TAG, "onRepeatClicked() called"); + // TODO: implement repeat all when playlist is implemented + + // Switch the modes between DISABLED and REPEAT_ONE, till playlist is implemented + setCurrentRepeatMode(getCurrentRepeatMode() == RepeatMode.REPEAT_DISABLED ? + RepeatMode.REPEAT_ONE : + RepeatMode.REPEAT_DISABLED); + + if (DEBUG) Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getCurrentRepeatMode().name()); + } + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + } + + @Override + public void onLoadingChanged(boolean isLoading) { + if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]"); + + if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning.get()) stopProgressLoop(); + else if (isLoading && !isProgressLoopRunning.get()) startProgressLoop(); + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (DEBUG) Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); + if (getCurrentState() == STATE_PAUSED_SEEK) { + if (DEBUG) Log.d(TAG, "onPlayerStateChanged() currently on PausedSeek"); + return; + } + + switch (playbackState) { + case ExoPlayer.STATE_IDLE: // 1 + isPrepared = false; + break; + case ExoPlayer.STATE_BUFFERING: // 2 + if (isPrepared && getCurrentState() != STATE_LOADING) changeState(STATE_BUFFERING); + break; + case ExoPlayer.STATE_READY: //3 + if (!isPrepared) { + isPrepared = true; + onPrepared(playWhenReady); + break; + } + if (currentState == STATE_PAUSED_SEEK) break; + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + break; + case ExoPlayer.STATE_ENDED: // 4 + changeState(STATE_COMPLETED); + isPrepared = false; + break; + } + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); + onError(error); + } + + @Override + public void onPositionDiscontinuity() { + } + + /*////////////////////////////////////////////////////////////////////////// + // General Player + //////////////////////////////////////////////////////////////////////////*/ + + public abstract void onError(Exception exception); + + public void onPrepared(boolean playWhenReady) { + if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); + if (playWhenReady) audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + } + + public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); + + public void onVideoPlayPause() { + if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); + + if (currentState == STATE_COMPLETED) { + onVideoPlayPauseRepeat(); + return; + } + + if (!isPlaying()) audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + else audioManager.abandonAudioFocus(null); + + simpleExoPlayer.setPlayWhenReady(!isPlaying()); + } + + public void onVideoPlayPauseRepeat() { + if (DEBUG) Log.d(TAG, "onVideoPlayPauseRepeat() called"); + changeState(STATE_LOADING); + setVideoStartPos(0); + simpleExoPlayer.seekTo(0); + simpleExoPlayer.setPlayWhenReady(true); + audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + } + + public void onFastRewind() { + if (DEBUG) Log.d(TAG, "onFastRewind() called"); + seekBy(-FAST_FORWARD_REWIND_AMOUNT); + } + + public void onFastForward() { + if (DEBUG) Log.d(TAG, "onFastForward() called"); + seekBy(FAST_FORWARD_REWIND_AMOUNT); + } + + public void onThumbnailReceived(Bitmap thumbnail) { + if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]"); + } + + public void seekBy(int milliSeconds) { + if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); + if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) return; + int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); + if (progress < 0) progress = 0; + simpleExoPlayer.seekTo(progress); + } + + public boolean isPlaying() { + return simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_READY && simpleExoPlayer.getPlayWhenReady(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private final StringBuilder stringBuilder = new StringBuilder(); + private final Formatter formatter = new Formatter(stringBuilder, Locale.getDefault()); + + public String getTimeString(int milliSeconds) { + long seconds = (milliSeconds % 60000L) / 1000L; + long minutes = (milliSeconds % 3600000L) / 60000L; + long hours = (milliSeconds % 86400000L) / 3600000L; + long days = (milliSeconds % (86400000L * 7L)) / 86400000L; + + stringBuilder.setLength(0); + return days > 0 ? formatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString() + : hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() + : formatter.format("%02d:%02d", minutes, seconds).toString(); + } + + protected void startProgressLoop() { + progressLoop.removeCallbacksAndMessages(null); + isProgressLoopRunning.set(true); + progressLoop.post(progressUpdate); + } + + protected void stopProgressLoop() { + isProgressLoopRunning.set(false); + progressLoop.removeCallbacksAndMessages(null); + } + + protected void tryDeleteCacheFiles(Context context) { + File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); + + if (cacheDir.exists()) { + try { + if (cacheDir.isDirectory()) { + for (File file : cacheDir.listFiles()) { + try { + if (DEBUG) Log.d(TAG, "tryDeleteCacheFiles: " + file.getAbsolutePath() + " deleted = " + file.delete()); + } catch (Exception ignored) { + } + } + } + } catch (Exception ignored) { + } + } + } + + public void triggerProgressUpdate() { + onUpdateProgress((int) simpleExoPlayer.getCurrentPosition(), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage()); + } + + public void animateAudio(final float from, final float to, int duration) { + ValueAnimator valueAnimator = new ValueAnimator(); + valueAnimator.setFloatValues(from, to); + valueAnimator.setDuration(duration); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (simpleExoPlayer != null) simpleExoPlayer.setVolume(from); + } + + @Override + public void onAnimationCancel(Animator animation) { + if (simpleExoPlayer != null) simpleExoPlayer.setVolume(to); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (simpleExoPlayer != null) simpleExoPlayer.setVolume(to); + } + }); + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (simpleExoPlayer != null) simpleExoPlayer.setVolume(((float) animation.getAnimatedValue())); + } + }); + valueAnimator.start(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Getters and Setters + //////////////////////////////////////////////////////////////////////////*/ + + public SimpleExoPlayer getPlayer() { + return simpleExoPlayer; + } + + public SharedPreferences getSharedPreferences() { + return sharedPreferences; + } + + public RepeatMode getCurrentRepeatMode() { + return currentRepeatMode; + } + + public void setCurrentRepeatMode(RepeatMode mode) { + currentRepeatMode = mode; + } + + public int getCurrentState() { + return currentState; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getVideoStartPos() { + return videoStartPos; + } + + public void setVideoStartPos(int videoStartPos) { + this.videoStartPos = videoStartPos; + } + + public String getVideoTitle() { + return videoTitle; + } + + public void setVideoTitle(String videoTitle) { + this.videoTitle = videoTitle; + } + + public String getChannelName() { + return channelName; + } + + public void setChannelName(String channelName) { + this.channelName = channelName; + } + + public boolean isCompleted() { + return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_ENDED; + } + + public boolean isPrepared() { + return isPrepared; + } + + public void setPrepared(boolean prepared) { + isPrepared = prepared; + } + + public Bitmap getVideoThumbnail() { + return videoThumbnail; + } + + public void setVideoThumbnail(Bitmap videoThumbnail) { + this.videoThumbnail = videoThumbnail; + } + + public String getVideoThumbnailUrl() { + return videoThumbnailUrl; + } + + public void setVideoThumbnailUrl(String videoThumbnailUrl) { + this.videoThumbnailUrl = videoThumbnailUrl; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java similarity index 82% rename from app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java rename to app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 4aabad3ac..c5f0b2d3b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -1,10 +1,8 @@ package org.schabi.newpipe.player; import android.app.Activity; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.graphics.Color; import android.media.AudioManager; @@ -23,22 +21,20 @@ import android.widget.TextView; import android.widget.Toast; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream_info.VideoStream; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; /** - * Activity Player implementing AbstractPlayer + * Activity Player implementing VideoPlayer * * @author mauriciocolli */ -public class ExoPlayerActivity extends Activity { - private static final String TAG = ".ExoPlayerActivity"; - private static final boolean DEBUG = AbstractPlayer.DEBUG; +public class MainVideoPlayer extends Activity { + private static final String TAG = ".MainVideoPlayer"; + private static final boolean DEBUG = BasePlayer.DEBUG; private AudioManager audioManager; - private BroadcastReceiver broadcastReceiver; private GestureDetector gestureDetector; private final Runnable hideUiRunnable = new Runnable() { @@ -49,7 +45,7 @@ public class ExoPlayerActivity extends Activity { }; private boolean activityPaused; - private AbstractPlayerImpl playerImpl; + private VideoPlayerImpl playerImpl; /*////////////////////////////////////////////////////////////////////////// // Activity LifeCycle @@ -72,9 +68,8 @@ public class ExoPlayerActivity extends Activity { showSystemUi(); setContentView(R.layout.activity_exo_player); - playerImpl = new AbstractPlayerImpl(); + playerImpl = new VideoPlayerImpl(); playerImpl.setup(findViewById(android.R.id.content)); - initReceiver(); playerImpl.handleIntent(getIntent()); } @@ -97,8 +92,10 @@ public class ExoPlayerActivity extends Activity { super.onStop(); if (DEBUG) Log.d(TAG, "onStop() called"); activityPaused = true; - playerImpl.destroy(); - playerImpl.setVideoStartPos((int) playerImpl.getPlayer().getCurrentPosition()); + if (playerImpl.getPlayer() != null) { + playerImpl.setVideoStartPos((int) playerImpl.getPlayer().getCurrentPosition()); + playerImpl.destroyPlayer(); + } } @Override @@ -106,9 +103,9 @@ public class ExoPlayerActivity extends Activity { super.onResume(); if (DEBUG) Log.d(TAG, "onResume() called"); if (activityPaused) { - playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); playerImpl.initPlayer(); - playerImpl.playVideo(playerImpl.getSelectedVideoStream(), false); + playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); + playerImpl.play(false); activityPaused = false; } } @@ -118,29 +115,6 @@ public class ExoPlayerActivity extends Activity { super.onDestroy(); if (DEBUG) Log.d(TAG, "onDestroy() called"); if (playerImpl != null) playerImpl.destroy(); - if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); - } - - /*////////////////////////////////////////////////////////////////////////// - // Init - //////////////////////////////////////////////////////////////////////////*/ - - private void initReceiver() { - if (DEBUG) Log.d(TAG, "initReceiver() called"); - broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (DEBUG) Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]"); - switch (intent.getAction()) { - case AbstractPlayer.ACTION_UPDATE_THUMB: - playerImpl.onUpdateThumbnail(intent); - break; - } - } - }; - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(AbstractPlayer.ACTION_UPDATE_THUMB); - registerReceiver(broadcastReceiver, intentFilter); } /*////////////////////////////////////////////////////////////////////////// @@ -182,7 +156,7 @@ public class ExoPlayerActivity extends Activity { /////////////////////////////////////////////////////////////////////////// @SuppressWarnings({"unused", "WeakerAccess"}) - private class AbstractPlayerImpl extends AbstractPlayer { + private class VideoPlayerImpl extends VideoPlayer { private TextView titleTextView; private TextView channelTextView; private TextView volumeTextView; @@ -192,8 +166,8 @@ public class ExoPlayerActivity extends Activity { private ImageButton screenRotationButton; private ImageButton playPauseButton; - AbstractPlayerImpl() { - super("AbstractPlayerImpl" + ExoPlayerActivity.TAG, ExoPlayerActivity.this); + VideoPlayerImpl() { + super("VideoPlayerImpl" + MainVideoPlayer.TAG, MainVideoPlayer.this); } @Override @@ -238,8 +212,8 @@ public class ExoPlayerActivity extends Activity { } @Override - public void playVideo(VideoStream videoStream, boolean autoPlay) { - super.playVideo(videoStream, autoPlay); + public void playUrl(String url, String format, boolean autoPlay) { + super.playUrl(url, format, autoPlay); playPauseButton.setImageResource(autoPlay ? R.drawable.ic_pause_white : R.drawable.ic_play_arrow_white); } @@ -249,16 +223,16 @@ public class ExoPlayerActivity extends Activity { if (playerImpl.getPlayer() == null) return; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && !PermissionHelper.checkSystemAlertWindowPermission(ExoPlayerActivity.this)) { - Toast.makeText(ExoPlayerActivity.this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); + && !PermissionHelper.checkSystemAlertWindowPermission(MainVideoPlayer.this)) { + Toast.makeText(MainVideoPlayer.this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); return; } - if (playerImpl != null) playerImpl.destroy(); - context.startService(NavigationHelper.getOpenPlayerIntent(context, PopupVideoPlayer.class, playerImpl)); + context.startService(NavigationHelper.getOpenVideoPlayerIntent(context, PopupVideoPlayer.class, playerImpl)); + if (playerImpl != null) playerImpl.destroyPlayer(); ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); - ExoPlayerActivity.this.finish(); + MainVideoPlayer.this.finish(); } @Override @@ -307,28 +281,6 @@ public class ExoPlayerActivity extends Activity { toggleOrientation(); } - @Override - public void onVideoPlayPause() { - super.onVideoPlayPause(); - if (getPlayer().getPlayWhenReady()) { - animateView(playPauseButton, false, 80, 0, new Runnable() { - @Override - public void run() { - playPauseButton.setImageResource(R.drawable.ic_pause_white); - animateView(playPauseButton, true, 200, 0); - } - }); - } else { - animateView(playPauseButton, false, 80, 0, new Runnable() { - @Override - public void run() { - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); - animateView(playPauseButton, true, 200, 0); - } - }); - } - } - @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); @@ -344,7 +296,8 @@ public class ExoPlayerActivity extends Activity { } @Override - public void onError() { + public void onError(Exception exception) { + exception.printStackTrace(); Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); finish(); } @@ -366,10 +319,37 @@ public class ExoPlayerActivity extends Activity { animateView(playPauseButton, false, 100, 0); } + @Override + public void onPlaying() { + super.onPlaying(); + //playPauseButton.setImageResource(R.drawable.ic_pause_white); + //animateView(playPauseButton, true, 500, 0); + + animateView(playPauseButton, false, 80, 0, new Runnable() { + @Override + public void run() { + playPauseButton.setImageResource(R.drawable.ic_pause_white); + animateView(playPauseButton, true, 200, 0); + } + }); + + showSystemUi(); + } + @Override public void onPaused() { super.onPaused(); - animateView(playPauseButton, true, 100, 0); + //playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + //animateView(playPauseButton, true, 100, 0); + + animateView(playPauseButton, false, 80, 0, new Runnable() { + @Override + public void run() { + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + animateView(playPauseButton, true, 200, 0); + } + }); + showSystemUi(); } @@ -379,12 +359,6 @@ public class ExoPlayerActivity extends Activity { animateView(playPauseButton, false, 100, 0); } - @Override - public void onPlaying() { - super.onPlaying(); - animateView(playPauseButton, true, 500, 0); - showSystemUi(); - } @Override public void onCompleted() { @@ -467,14 +441,14 @@ public class ExoPlayerActivity extends Activity { @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - if (playerImpl.getCurrentState() != StateInterface.STATE_PLAYING) return true; + if (playerImpl.getCurrentState() != BasePlayer.STATE_PLAYING) return true; if (playerImpl.isControlsVisible()) playerImpl.animateView(playerImpl.getControlsRoot(), false, 150, 0, true); else { playerImpl.animateView(playerImpl.getControlsRoot(), true, 500, 0, new Runnable() { @Override public void run() { - playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME, true); + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME, true); } }); showSystemUi(); @@ -482,25 +456,25 @@ public class ExoPlayerActivity extends Activity { return true; } - private final float stepsBrightness = 21, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; + private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; private float currentBrightness = .5f; private int currentVolume, maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0; private final String brightnessUnicode = new String(Character.toChars(0x2600)); - // private final String volumeUnicode = new String(Character.toChars(0x1F50A)); private final String volumeUnicode = new String(Character.toChars(0x1F508)); - private final int MOVEMENT_THRESHOLD = 40; - private final int eventsThreshold = 3; + private final int eventsThreshold = 8; private boolean triggered = false; private int eventsNum; + // TODO: Improve video gesture controls @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //noinspection PointlessBooleanExpression - if (DEBUG && false) Log.d(TAG, "ExoPlayerActivity.onScroll = " + + if (DEBUG && true) Log.d(TAG, "MainVideoPlayer.onScroll = " + ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + ", distanceXy = [" + distanceX + ", " + distanceY + "]"); @@ -510,16 +484,17 @@ public class ExoPlayerActivity extends Activity { return false; } - if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == StateInterface.STATE_COMPLETED) return false; + if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) return false; isMoving = true; // boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top boolean up = distanceY > 0; if (e1.getX() > playerImpl.getRootView().getWidth() / 2) { - currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + (up ? 1 : -1); + double floor = Math.floor(up ? stepVolume : -stepVolume); + currentVolume = (int) (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + floor); if (currentVolume >= maxVolume) currentVolume = maxVolume; - if (currentVolume <= 0) currentVolume = 0; + if (currentVolume <= minVolume) currentVolume = (int) minVolume; audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0); if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); @@ -555,8 +530,8 @@ public class ExoPlayerActivity extends Activity { if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.animateView(playerImpl.getVolumeTextView(), false, 200, 200); if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.animateView(playerImpl.getBrightnessTextView(), false, 200, 200); - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == StateInterface.STATE_PLAYING) { - playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME); + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) { + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 216c4eb74..170e2f6aa 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -4,7 +4,6 @@ import android.annotation.SuppressLint; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -31,7 +30,6 @@ import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; -import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; @@ -39,7 +37,6 @@ import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.extractor.stream_info.VideoStream; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -47,13 +44,13 @@ import org.schabi.newpipe.util.Utils; import org.schabi.newpipe.workers.StreamExtractorWorker; /** - * Service Popup Player implementing AbstractPlayer + * Service Popup Player implementing VideoPlayer * * @author mauriciocolli */ public class PopupVideoPlayer extends Service { private static final String TAG = ".PopupVideoPlayer"; - private static final boolean DEBUG = AbstractPlayer.DEBUG; + private static final boolean DEBUG = BasePlayer.DEBUG; private static final int NOTIFICATION_ID = 40028922; public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; @@ -61,8 +58,6 @@ public class PopupVideoPlayer extends Service { public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL"; public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT"; - private BroadcastReceiver broadcastReceiver; - private WindowManager windowManager; private WindowManager.LayoutParams windowLayoutParams; private GestureDetector gestureDetector; @@ -81,7 +76,7 @@ public class PopupVideoPlayer extends Service { private ImageLoader imageLoader = ImageLoader.getInstance(); private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); - private AbstractPlayerImpl playerImpl; + private VideoPlayerImpl playerImpl; private StreamExtractorWorker currentExtractorWorker; /*////////////////////////////////////////////////////////////////////////// @@ -92,9 +87,8 @@ public class PopupVideoPlayer extends Service { public void onCreate() { windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - initReceiver(); - playerImpl = new AbstractPlayerImpl(); + playerImpl = new VideoPlayerImpl(); ThemeHelper.setTheme(this, false); } @@ -132,7 +126,6 @@ public class PopupVideoPlayer extends Service { } if (imageLoader != null) imageLoader.clearMemoryCache(); if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); - if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); if (currentExtractorWorker != null) { currentExtractorWorker.cancel(); currentExtractorWorker = null; @@ -148,39 +141,6 @@ public class PopupVideoPlayer extends Service { // Init //////////////////////////////////////////////////////////////////////////*/ - private void initReceiver() { - broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (DEBUG) Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]"); - switch (intent.getAction()) { - case ACTION_CLOSE: - onVideoClose(); - break; - case ACTION_PLAY_PAUSE: - playerImpl.onVideoPlayPause(); - break; - case ACTION_OPEN_DETAIL: - onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl(), playerImpl.getVideoTitle()); - break; - case ACTION_REPEAT: - playerImpl.onRepeatClicked(); - break; - case AbstractPlayer.ACTION_UPDATE_THUMB: - playerImpl.onUpdateThumbnail(intent); - break; - } - } - }; - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(ACTION_CLOSE); - intentFilter.addAction(ACTION_PLAY_PAUSE); - intentFilter.addAction(ACTION_OPEN_DETAIL); - intentFilter.addAction(ACTION_REPEAT); - intentFilter.addAction(AbstractPlayer.ACTION_UPDATE_THUMB); - registerReceiver(broadcastReceiver, intentFilter); - } - @SuppressLint("RtlHardcoded") private void initPopup() { if (DEBUG) Log.d(TAG, "initPopup() called"); @@ -213,8 +173,9 @@ public class PopupVideoPlayer extends Service { private NotificationCompat.Builder createNotification() { notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification); - if (playerImpl.getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail()); - else notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); + if (playerImpl.getVideoThumbnail() == null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); + else notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail()); + notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName()); @@ -302,21 +263,35 @@ public class PopupVideoPlayer extends Service { /////////////////////////////////////////////////////////////////////////// - private class AbstractPlayerImpl extends AbstractPlayer { - AbstractPlayerImpl() { - super("AbstractPlayerImpl" + PopupVideoPlayer.TAG, PopupVideoPlayer.this); + private class VideoPlayerImpl extends VideoPlayer { + VideoPlayerImpl() { + super("VideoPlayerImpl" + PopupVideoPlayer.TAG, PopupVideoPlayer.this); } @Override - public void playVideo(VideoStream videoStream, boolean autoPlay) { - super.playVideo(videoStream, autoPlay); + public void playUrl(String url, String format, boolean autoPlay) { + super.playUrl(url, format, autoPlay); windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight); windowManager.updateViewLayout(getRootView(), windowLayoutParams); notBuilder = createNotification(); startForeground(NOTIFICATION_ID, notBuilder.build()); - notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); + } + + @Override + public void destroy() { + super.destroy(); + if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null); + } + + @Override + public void onThumbnailReceived(Bitmap thumbnail) { + super.onThumbnailReceived(thumbnail); + if (thumbnail != null) { + if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); + updateNotification(-1); + } } @Override @@ -324,8 +299,8 @@ public class PopupVideoPlayer extends Service { if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); Intent intent; if (!getSharedPreferences().getBoolean(getResources().getString(R.string.use_old_player_key), false)) { - intent = NavigationHelper.getOpenPlayerIntent(context, ExoPlayerActivity.class, playerImpl); - if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(AbstractPlayer.STARTED_FROM_NEWPIPE, false); + intent = NavigationHelper.getOpenVideoPlayerIntent(context, MainVideoPlayer.class, playerImpl); + if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(VideoPlayer.STARTED_FROM_NEWPIPE, false); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } else { intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class) @@ -335,9 +310,9 @@ public class PopupVideoPlayer extends Service { .putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - stopSelf(); - if (playerImpl != null) playerImpl.destroy(); context.startActivity(intent); + if (playerImpl != null) playerImpl.destroyPlayer(); + stopSelf(); } @Override @@ -360,13 +335,6 @@ public class PopupVideoPlayer extends Service { updateNotification(-1); } - @Override - public void onUpdateThumbnail(Intent intent) { - super.onUpdateThumbnail(intent); - if (getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, getVideoThumbnail()); - updateNotification(-1); - } - @Override public void onDismiss(PopupMenu menu) { super.onDismiss(menu); @@ -374,11 +342,45 @@ public class PopupVideoPlayer extends Service { } @Override - public void onError() { + public void onError(Exception exception) { + exception.printStackTrace(); Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); stopSelf(); } + /*////////////////////////////////////////////////////////////////////////// + // Broadcast Receiver + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void setupBroadcastReceiver(IntentFilter intentFilter) { + super.setupBroadcastReceiver(intentFilter); + if (DEBUG) Log.d(TAG, "setupBroadcastReceiver() called with: intentFilter = [" + intentFilter + "]"); + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_OPEN_DETAIL); + intentFilter.addAction(ACTION_REPEAT); + } + + @Override + public void onBroadcastReceived(Intent intent) { + super.onBroadcastReceived(intent); + if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + switch (intent.getAction()) { + case ACTION_CLOSE: + onVideoClose(); + break; + case ACTION_PLAY_PAUSE: + playerImpl.onVideoPlayPause(); + break; + case ACTION_OPEN_DETAIL: + onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl(), playerImpl.getVideoTitle()); + break; + case ACTION_REPEAT: + playerImpl.onRepeatClicked(); + break; + } + } /*////////////////////////////////////////////////////////////////////////// // States //////////////////////////////////////////////////////////////////////////*/ @@ -483,8 +485,8 @@ public class PopupVideoPlayer extends Service { private void onScrollEnd() { if (DEBUG) Log.d(TAG, "onScrollEnd() called"); - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == StateInterface.STATE_PLAYING) { - playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME); + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) { + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME); } } @@ -516,6 +518,7 @@ public class PopupVideoPlayer extends Service { public void onReceive(StreamInfo info) { playerImpl.setVideoTitle(info.title); playerImpl.setVideoUrl(info.webpage_url); + playerImpl.setVideoThumbnailUrl(info.thumbnail_url); playerImpl.setChannelName(info.uploader); playerImpl.setVideoStreamsList(Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)); @@ -537,7 +540,7 @@ public class PopupVideoPlayer extends Service { mainHandler.post(new Runnable() { @Override public void run() { - playerImpl.playVideo(playerImpl.getSelectedVideoStream(), true); + playerImpl.play(true); } }); @@ -551,7 +554,6 @@ public class PopupVideoPlayer extends Service { playerImpl.setVideoThumbnail(loadedImage); if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); updateNotification(-1); - ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = loadedImage; } }); } @@ -619,6 +621,7 @@ public class PopupVideoPlayer extends Service { @Override public void onUnrecoverableError(Exception exception) { + exception.printStackTrace(); stopSelf(); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/StateInterface.java b/app/src/main/java/org/schabi/newpipe/player/StateInterface.java deleted file mode 100644 index 7b3681ab8..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/StateInterface.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.schabi.newpipe.player; - -public interface StateInterface { - int STATE_LOADING = 123; - int STATE_PLAYING = 124; - int STATE_BUFFERING = 125; - int STATE_PAUSED = 126; - int STATE_PAUSED_SEEK = 127; - int STATE_COMPLETED = 128; - - void changeState(int state); - - void onLoading(); - void onPlaying(); - void onBuffering(); - void onPaused(); - void onPausedSeek(); - void onCompleted(); -} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java similarity index 61% rename from app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java rename to app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 2faf09cc3..3e0976242 100644 --- a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -7,16 +7,12 @@ import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; import android.net.Uri; import android.os.Build; -import android.os.Handler; -import android.preference.PreferenceManager; import android.support.v4.content.ContextCompat; -import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -29,84 +25,42 @@ import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.cache.CacheDataSource; -import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; -import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; -import com.google.android.exoplayer2.upstream.cache.SimpleCache; -import com.google.android.exoplayer2.util.Util; -import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream_info.AudioStream; import org.schabi.newpipe.extractor.stream_info.VideoStream; -import java.io.File; import java.io.Serializable; import java.util.ArrayList; -import java.util.Formatter; import java.util.List; -import java.util.Locale; import java.util.Vector; -import java.util.concurrent.atomic.AtomicBoolean; /** - * Common properties of the players + * Base for video players * * @author mauriciocolli */ -@SuppressWarnings({"unused", "WeakerAccess"}) -public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBarChangeListener, View.OnClickListener, ExoPlayer.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener, SimpleExoPlayer.VideoListener { - public static final boolean DEBUG = false; +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, ExoPlayer.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener { + public static final boolean DEBUG = BasePlayer.DEBUG; public final String TAG; - protected Context context; - private SharedPreferences sharedPreferences; - - private static int currentState = -1; - public static final String ACTION_UPDATE_THUMB = "org.schabi.newpipe.player.AbstractPlayer.UPDATE_THUMBNAIL"; - /*////////////////////////////////////////////////////////////////////////// // Intent //////////////////////////////////////////////////////////////////////////*/ - public static final String VIDEO_URL = "video_url"; public static final String VIDEO_STREAMS_LIST = "video_streams_list"; public static final String VIDEO_ONLY_AUDIO_STREAM = "video_only_audio_stream"; - public static final String VIDEO_TITLE = "video_title"; public static final String INDEX_SEL_VIDEO_STREAM = "index_selected_video_stream"; - public static final String START_POSITION = "start_position"; - public static final String CHANNEL_NAME = "channel_name"; public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe"; - private String videoUrl = ""; - private int videoStartPos = -1; - private String videoTitle = ""; - private Bitmap videoThumbnail; - private String channelName = ""; private int selectedIndexStream; private ArrayList videoStreamsList = new ArrayList<>(); private AudioStream videoOnlyAudioStream; @@ -115,36 +69,10 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa // Player //////////////////////////////////////////////////////////////////////////*/ - public static final int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds public static final int DEFAULT_CONTROLS_HIDE_TIME = 3000; // 3 Seconds - public static final String CACHE_FOLDER_NAME = "exoplayer"; private boolean startedFromNewPipe = true; - private boolean isPrepared = false; private boolean wasPlaying = false; - private SimpleExoPlayer simpleExoPlayer; - - @SuppressWarnings("FieldCanBeLocal") - private MediaSource videoSource; - private static CacheDataSourceFactory cacheDataSourceFactory; - private static final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); - private static final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - - private AtomicBoolean isProgressLoopRunning = new AtomicBoolean(); - private Handler progressLoop; - private Runnable progressUpdate; - - /*////////////////////////////////////////////////////////////////////////// - // Repeat - //////////////////////////////////////////////////////////////////////////*/ - - private RepeatMode currentRepeatMode = RepeatMode.REPEAT_DISABLED; - - public enum RepeatMode { - REPEAT_DISABLED, - REPEAT_ONE, - REPEAT_ALL - } /*////////////////////////////////////////////////////////////////////////// // Views @@ -181,35 +109,15 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa /////////////////////////////////////////////////////////////////////////// - public AbstractPlayer(String debugTag, Context context) { + public VideoPlayer(String debugTag, Context context) { + super(context); this.TAG = debugTag; this.context = context; - this.progressLoop = new Handler(); - this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - - if (cacheDataSourceFactory == null) { - DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getPackageName()), bandwidthMeter); - File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); - if (!cacheDir.exists()) { - //noinspection ResultOfMethodCallIgnored - cacheDir.mkdir(); - } - - Log.d(TAG, "buildMediaSource: cacheDir = " + cacheDir.getAbsolutePath()); - SimpleCache simpleCache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(64 * 1024 * 1024L)); - cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory, CacheDataSource.FLAG_BLOCK_ON_CACHE, 512 * 1024); - } } public void setup(View rootView) { initViews(rootView); - initListeners(); - if (simpleExoPlayer == null) initPlayer(); - else { - simpleExoPlayer.addListener(this); - simpleExoPlayer.setVideoListener(this); - simpleExoPlayer.setVideoSurfaceView(surfaceView); - } + setup(); } public void initViews(View rootView) { @@ -241,36 +149,24 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa } + @Override public void initListeners() { - progressUpdate = new Runnable() { - @Override - public void run() { - //if(DEBUG) Log.d(TAG, "progressUpdate run() called"); - onUpdateProgress((int) simpleExoPlayer.getCurrentPosition(), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage()); - if (isProgressLoopRunning.get()) progressLoop.postDelayed(this, 100); - } - }; - + super.initListeners(); playbackSeekBar.setOnSeekBarChangeListener(this); fullScreenButton.setOnClickListener(this); qualityTextView.setOnClickListener(this); } + @Override public void initPlayer() { - if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); - - AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); - DefaultTrackSelector defaultTrackSelector = new DefaultTrackSelector(trackSelectionFactory); - DefaultLoadControl loadControl = new DefaultLoadControl(); - - simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, defaultTrackSelector, loadControl); - simpleExoPlayer.addListener(this); - simpleExoPlayer.setVideoListener(this); + super.initPlayer(); simpleExoPlayer.setVideoSurfaceView(surfaceView); + simpleExoPlayer.setVideoListener(this); } @SuppressWarnings("unchecked") public void handleIntent(Intent intent) { + super.handleIntent(intent); if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); if (intent == null) return; @@ -284,79 +180,36 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa Serializable audioStream = intent.getSerializableExtra(VIDEO_ONLY_AUDIO_STREAM); if (audioStream != null) videoOnlyAudioStream = (AudioStream) audioStream; - videoUrl = intent.getStringExtra(VIDEO_URL); - videoTitle = intent.getStringExtra(VIDEO_TITLE); - videoStartPos = intent.getIntExtra(START_POSITION, -1); - channelName = intent.getStringExtra(CHANNEL_NAME); startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true); - try { - videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; - } catch (Exception e) { - e.printStackTrace(); - } - - playVideo(getSelectedVideoStream(), true); + play(true); } - public void playVideo(VideoStream videoStream, boolean autoPlay) { - if (DEBUG) { - Log.d(TAG, "playVideo() called with: videoStream = [" + videoStream + ", " + videoStream.url + ", isVideoOnly = " + videoStream.isVideoOnly + "], autoPlay = [" + autoPlay + "]"); - } - if (videoStream == null || videoStream.url == null || simpleExoPlayer == null) { - onError(); - return; - } + public void play(boolean autoPlay) { + playUrl(getSelectedVideoStream().url, MediaFormat.getSuffixById(getSelectedVideoStream().format), autoPlay); + } - isPrepared = false; + @Override + public void playUrl(String url, String format, boolean autoPlay) { + if (DEBUG) Log.d(TAG, "play() called with: url = [" + url + "], autoPlay = [" + autoPlay + "]"); qualityChanged = false; + if (url == null || simpleExoPlayer == null) { + RuntimeException runtimeException = new RuntimeException((url == null ? "Url " : "Player ") + " null"); + onError(runtimeException); + throw runtimeException; + } + qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); buildQualityMenu(qualityPopupMenu); - videoSource = buildMediaSource(videoStream, MediaFormat.getSuffixById(getSelectedVideoStream().format)); - - if (simpleExoPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) simpleExoPlayer.stop(); - if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); - simpleExoPlayer.prepare(videoSource); - simpleExoPlayer.setPlayWhenReady(autoPlay); - changeState(STATE_LOADING); + super.playUrl(url, format, autoPlay); } - public void destroy() { - if (DEBUG) Log.d(TAG, "destroy() called"); - if (simpleExoPlayer != null) { - simpleExoPlayer.stop(); - simpleExoPlayer.release(); - } - if (progressLoop != null) stopProgressLoop(); - } - - private MediaSource buildMediaSource(VideoStream videoStream, String overrideExtension) { - if (DEBUG) { - Log.d(TAG, "buildMediaSource() called with: videoStream = [" + videoStream + ", " + videoStream.url + "isVideoOnly = " + videoStream.isVideoOnly + "], overrideExtension = [" + overrideExtension + "]"); - } - Uri uri = Uri.parse(videoStream.url); - int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); - MediaSource mediaSource; - switch (type) { - case C.TYPE_SS: - mediaSource = new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null); - break; - case C.TYPE_DASH: - mediaSource = new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null); - break; - case C.TYPE_HLS: - mediaSource = new HlsMediaSource(uri, cacheDataSourceFactory, null, null); - break; - case C.TYPE_OTHER: - mediaSource = new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null); - break; - default: { - throw new IllegalStateException("Unsupported type: " + type); - } - } - if (!videoStream.isVideoOnly) return mediaSource; + @Override + public MediaSource buildMediaSource(String url, String overrideExtension) { + MediaSource mediaSource = super.buildMediaSource(url, overrideExtension); + if (!getSelectedVideoStream().isVideoOnly) return mediaSource; Uri audioUri = Uri.parse(videoOnlyAudioStream.url); return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null)); @@ -370,39 +223,12 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa qualityTextView.setText(getSelectedVideoStream().resolution); popupMenu.setOnMenuItemClickListener(this); popupMenu.setOnDismissListener(this); - } /*////////////////////////////////////////////////////////////////////////// // States Implementation //////////////////////////////////////////////////////////////////////////*/ - @Override - public void changeState(int state) { - if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); - currentState = state; - switch (state) { - case STATE_LOADING: - onLoading(); - break; - case STATE_PLAYING: - onPlaying(); - break; - case STATE_BUFFERING: - onBuffering(); - break; - case STATE_PAUSED: - onPaused(); - break; - case STATE_PAUSED_SEEK: - onPausedSeek(); - break; - case STATE_COMPLETED: - onCompleted(); - break; - } - } - @Override public void onLoading() { if (DEBUG) Log.d(TAG, "onLoading() called"); @@ -463,7 +289,6 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (isProgressLoopRunning.get()) stopProgressLoop(); - if (videoThumbnail != null) endScreen.setImageBitmap(videoThumbnail); animateView(controlsRoot, true, 500, 0); animateView(endScreen, true, 800, 0); animateView(currentDisplaySeek, false, 200, 0); @@ -481,74 +306,10 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (currentRepeatMode == RepeatMode.REPEAT_ONE) { changeState(STATE_LOADING); - getPlayer().seekTo(0); + simpleExoPlayer.seekTo(0); } } - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - - } - - @Override - public void onLoadingChanged(boolean isLoading) { - if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]"); - - if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning.get()) stopProgressLoop(); - else if (isLoading && !isProgressLoopRunning.get()) startProgressLoop(); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (DEBUG) Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); - if (getCurrentState() == STATE_PAUSED_SEEK) { - if (DEBUG) Log.d(TAG, "onPlayerStateChanged() currently on PausedSeek"); - return; - } - - switch (playbackState) { - case ExoPlayer.STATE_IDLE: // 1 - isPrepared = false; - break; - case ExoPlayer.STATE_BUFFERING: // 2 - if (isPrepared && getCurrentState() != STATE_LOADING) changeState(STATE_BUFFERING); - break; - case ExoPlayer.STATE_READY: //3 - if (!isPrepared) { - isPrepared = true; - onPrepared(playWhenReady); - break; - } - if (currentState == STATE_PAUSED_SEEK) break; - changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - break; - case ExoPlayer.STATE_ENDED: // 4 - changeState(STATE_COMPLETED); - isPrepared = false; - break; - } - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); - onError(); - } - - @Override - public void onPositionDiscontinuity() { - if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called"); - } - /*////////////////////////////////////////////////////////////////////////// // ExoPlayer Video Listener //////////////////////////////////////////////////////////////////////////*/ @@ -570,8 +331,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa // General Player //////////////////////////////////////////////////////////////////////////*/ - public abstract void onError(); - + @Override public void onPrepared(boolean playWhenReady) { if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); @@ -584,11 +344,19 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); - changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + super.onPrepared(playWhenReady); } + @Override + public void destroy() { + super.destroy(); + if (endScreen != null) endScreen.setImageBitmap(null); + } + + @Override public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { if (!isPrepared) return; + if (currentState != STATE_PAUSED) { if (currentState != STATE_PAUSED_SEEK) playbackSeekBar.setProgress(currentProgress); playbackCurrentTime.setText(getTimeString(currentProgress)); @@ -601,32 +369,32 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa } } - public void onUpdateThumbnail(Intent intent) { - if (DEBUG) Log.d(TAG, "onUpdateThumbnail() called with: intent = [" + intent + "]"); - if (!intent.getStringExtra(VIDEO_URL).equals(videoUrl)) return; - videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; + @Override + public void onVideoPlayPauseRepeat() { + if (DEBUG) Log.d(TAG, "onVideoPlayPauseRepeat() called"); + if (qualityChanged) { + setVideoStartPos(0); + play(true); + } else super.onVideoPlayPauseRepeat(); } - public void onVideoPlayPause() { - if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); - if (currentState == STATE_COMPLETED) { - changeState(STATE_LOADING); - if (qualityChanged) playVideo(getSelectedVideoStream(), true); - simpleExoPlayer.seekTo(0); - return; - } - simpleExoPlayer.setPlayWhenReady(!isPlaying()); + @Override + public void onThumbnailReceived(Bitmap thumbnail) { + super.onThumbnailReceived(thumbnail); + if (thumbnail != null) endScreen.setImageBitmap(thumbnail); } + protected abstract void onFullScreenButtonClicked(); + + @Override public void onFastRewind() { - if (DEBUG) Log.d(TAG, "onFastRewind() called"); - seekBy(-FAST_FORWARD_REWIND_AMOUNT); + super.onFastRewind(); showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true); } + @Override public void onFastForward() { - if (DEBUG) Log.d(TAG, "onFastForward() called"); - seekBy(FAST_FORWARD_REWIND_AMOUNT); + super.onFastForward(); showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true); } @@ -651,10 +419,10 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa public boolean onMenuItemClick(MenuItem menuItem) { if (DEBUG) Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]"); if (selectedIndexStream == menuItem.getItemId()) return true; - setVideoStartPos((int) getPlayer().getCurrentPosition()); + setVideoStartPos((int) simpleExoPlayer.getCurrentPosition()); selectedIndexStream = menuItem.getItemId(); - if (!(getCurrentState() == STATE_COMPLETED)) playVideo(getSelectedVideoStream(), wasPlaying); + if (!(getCurrentState() == STATE_COMPLETED)) play(wasPlaying); else qualityChanged = true; qualityTextView.setText(menuItem.getTitle()); @@ -671,8 +439,6 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa qualityTextView.setText(getSelectedVideoStream().resolution); } - public abstract void onFullScreenButtonClicked(); - public void onQualitySelectorClicked() { if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called"); qualityPopupMenu.show(); @@ -684,18 +450,6 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa wasPlaying = isPlaying(); } - public void onRepeatClicked() { - if (DEBUG) Log.d(TAG, "onRepeatClicked() called"); - // TODO: implement repeat all when playlist is implemented - - // Switch the modes between DISABLED and REPEAT_ONE, till playlist is implemented - setCurrentRepeatMode(getCurrentRepeatMode() == RepeatMode.REPEAT_DISABLED ? - RepeatMode.REPEAT_ONE : - RepeatMode.REPEAT_DISABLED); - - if (DEBUG) Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getCurrentRepeatMode().name()); - } - /*////////////////////////////////////////////////////////////////////////// // SeekBar Listener //////////////////////////////////////////////////////////////////////////*/ @@ -737,21 +491,6 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa // Utils //////////////////////////////////////////////////////////////////////////*/ - private static final StringBuilder stringBuilder = new StringBuilder(); - private static final Formatter formatter = new Formatter(stringBuilder, Locale.getDefault()); - - public String getTimeString(int milliSeconds) { - long seconds = (milliSeconds % 60000L) / 1000L; - long minutes = (milliSeconds % 3600000L) / 60000L; - long hours = (milliSeconds % 86400000L) / 3600000L; - long days = (milliSeconds % (86400000L * 7L)) / 86400000L; - - stringBuilder.setLength(0); - return days > 0 ? formatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString() - : hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() - : formatter.format("%02d:%02d", minutes, seconds).toString(); - } - public boolean isControlsVisible() { return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE; } @@ -908,62 +647,14 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa } } - private void seekBy(int milliSeconds) { - if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); - if (simpleExoPlayer == null) return; - int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); - simpleExoPlayer.seekTo(progress); - } - - public boolean isPlaying() { - return simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_READY && simpleExoPlayer.getPlayWhenReady(); - } - public boolean isQualityMenuVisible() { return isQualityPopupMenuVisible; } - private void startProgressLoop() { - progressLoop.removeCallbacksAndMessages(null); - isProgressLoopRunning.set(true); - progressLoop.post(progressUpdate); - } - - private void stopProgressLoop() { - isProgressLoopRunning.set(false); - progressLoop.removeCallbacksAndMessages(null); - } - - public void tryDeleteCacheFiles(Context context) { - File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); - - if (cacheDir.exists()) { - try { - if (cacheDir.isDirectory()) { - for (File file : cacheDir.listFiles()) { - try { - if (DEBUG) Log.d(TAG, "tryDeleteCacheFiles: " + file.getAbsolutePath() + " deleted = " + file.delete()); - } catch (Exception ignored) { - } - } - } - } catch (Exception ignored) { - } - } - } - /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ - public SimpleExoPlayer getPlayer() { - return simpleExoPlayer; - } - - public SharedPreferences getSharedPreferences() { - return sharedPreferences; - } - public AspectRatioFrameLayout getAspectRatioFrameLayout() { return aspectRatioFrameLayout; } @@ -972,22 +663,10 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa return surfaceView; } - public RepeatMode getCurrentRepeatMode() { - return currentRepeatMode; - } - - public void setCurrentRepeatMode(RepeatMode mode) { - currentRepeatMode = mode; - } - public boolean wasPlaying() { return wasPlaying; } - public int getCurrentState() { - return currentState; - } - public VideoStream getSelectedVideoStream() { return videoStreamsList.get(selectedIndexStream); } @@ -1000,46 +679,6 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa return qualityPopupMenuGroupId; } - public String getVideoUrl() { - return videoUrl; - } - - public void setVideoUrl(String videoUrl) { - this.videoUrl = videoUrl; - } - - public int getVideoStartPos() { - return videoStartPos; - } - - public void setVideoStartPos(int videoStartPos) { - this.videoStartPos = videoStartPos; - } - - public String getVideoTitle() { - return videoTitle; - } - - public void setVideoTitle(String videoTitle) { - this.videoTitle = videoTitle; - } - - public Bitmap getVideoThumbnail() { - return videoThumbnail; - } - - public void setVideoThumbnail(Bitmap videoThumbnail) { - this.videoThumbnail = videoThumbnail; - } - - public String getChannelName() { - return channelName; - } - - public void setChannelName(String channelName) { - this.channelName = channelName; - } - public int getSelectedStreamIndex() { return selectedIndexStream; } @@ -1135,4 +774,5 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa public TextView getCurrentDisplaySeek() { return currentDisplaySeek; } + }