2017-09-03 08:04:18 +02:00
|
|
|
/*
|
|
|
|
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
|
|
|
* BasePlayer.java is part of NewPipe
|
|
|
|
*
|
|
|
|
* License: GPL-3.0+
|
|
|
|
* This program 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.
|
|
|
|
*
|
|
|
|
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
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;
|
2017-09-24 06:50:32 +02:00
|
|
|
import android.media.audiofx.AudioEffect;
|
2017-04-17 06:19:53 +02:00
|
|
|
import android.net.Uri;
|
|
|
|
import android.preference.PreferenceManager;
|
2017-09-24 22:44:31 +02:00
|
|
|
import android.support.annotation.Nullable;
|
2017-04-17 06:19:53 +02:00
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.Log;
|
2017-09-08 06:47:43 +02:00
|
|
|
import android.view.View;
|
2017-04-17 06:19:53 +02:00
|
|
|
|
|
|
|
import com.google.android.exoplayer2.C;
|
|
|
|
import com.google.android.exoplayer2.DefaultLoadControl;
|
2017-08-18 20:07:57 +02:00
|
|
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
2017-04-17 06:19:53 +02:00
|
|
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
|
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
2017-09-24 06:50:32 +02:00
|
|
|
import com.google.android.exoplayer2.Format;
|
2017-06-26 04:41:52 +02:00
|
|
|
import com.google.android.exoplayer2.PlaybackParameters;
|
2017-08-18 20:07:57 +02:00
|
|
|
import com.google.android.exoplayer2.Player;
|
|
|
|
import com.google.android.exoplayer2.RenderersFactory;
|
2017-04-17 06:19:53 +02:00
|
|
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
|
|
|
import com.google.android.exoplayer2.Timeline;
|
2017-09-24 06:50:32 +02:00
|
|
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
|
|
|
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
2017-04-17 06:19:53 +02:00
|
|
|
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;
|
2017-09-08 06:47:43 +02:00
|
|
|
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
2017-04-17 06:19:53 +02:00
|
|
|
|
2017-09-03 08:04:18 +02:00
|
|
|
import org.schabi.newpipe.Downloader;
|
2017-08-07 15:04:36 +02:00
|
|
|
import org.schabi.newpipe.R;
|
2017-09-05 21:27:12 +02:00
|
|
|
import org.schabi.newpipe.extractor.InfoItem;
|
2017-09-04 19:23:56 +02:00
|
|
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
2017-09-05 21:27:12 +02:00
|
|
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
2017-09-20 06:46:16 +02:00
|
|
|
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
2017-09-06 08:49:00 +02:00
|
|
|
import org.schabi.newpipe.player.playback.PlaybackListener;
|
2017-09-05 21:27:12 +02:00
|
|
|
import org.schabi.newpipe.playlist.ExternalPlayQueue;
|
2017-08-31 19:07:18 +02:00
|
|
|
import org.schabi.newpipe.playlist.PlayQueue;
|
2017-09-05 21:27:12 +02:00
|
|
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
|
|
|
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
2017-08-07 15:04:36 +02:00
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
import java.io.File;
|
2017-09-05 21:27:12 +02:00
|
|
|
import java.io.Serializable;
|
2017-06-26 04:41:52 +02:00
|
|
|
import java.text.DecimalFormat;
|
|
|
|
import java.text.NumberFormat;
|
2017-09-05 21:27:12 +02:00
|
|
|
import java.util.ArrayList;
|
2017-04-17 06:19:53 +02:00
|
|
|
import java.util.Formatter;
|
2017-09-05 21:27:12 +02:00
|
|
|
import java.util.List;
|
2017-04-17 06:19:53 +02:00
|
|
|
import java.util.Locale;
|
2017-09-07 22:01:02 +02:00
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
import io.reactivex.Observable;
|
|
|
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
|
|
import io.reactivex.annotations.NonNull;
|
|
|
|
import io.reactivex.disposables.Disposable;
|
|
|
|
import io.reactivex.functions.Consumer;
|
|
|
|
import io.reactivex.functions.Predicate;
|
2017-04-17 06:19:53 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Base for the players, joining the common properties
|
|
|
|
*
|
|
|
|
* @author mauriciocolli
|
|
|
|
*/
|
|
|
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
2017-08-31 19:07:18 +02:00
|
|
|
public abstract class BasePlayer implements Player.EventListener,
|
2017-09-24 06:50:32 +02:00
|
|
|
AudioManager.OnAudioFocusChangeListener, PlaybackListener, AudioRendererEventListener {
|
2017-09-03 08:04:18 +02:00
|
|
|
// TODO: Check api version for deprecated audio manager methods
|
2017-09-03 04:30:34 +02:00
|
|
|
|
2017-09-07 22:01:02 +02:00
|
|
|
public static final boolean DEBUG = true;
|
2017-04-17 06:19:53 +02:00
|
|
|
public static final String TAG = "BasePlayer";
|
|
|
|
|
|
|
|
protected Context context;
|
|
|
|
protected SharedPreferences sharedPreferences;
|
|
|
|
protected AudioManager audioManager;
|
|
|
|
|
|
|
|
protected BroadcastReceiver broadcastReceiver;
|
|
|
|
protected IntentFilter intentFilter;
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Intent
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
public static final String INTENT_TYPE = "intent_type";
|
|
|
|
public static final String SINGLE_STREAM = "single";
|
|
|
|
public static final String EXTERNAL_PLAYLIST = "external";
|
|
|
|
public static final String INTERNAL_PLAYLIST = "internal";
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
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";
|
2017-06-26 04:41:52 +02:00
|
|
|
public static final String PLAYBACK_SPEED = "playback_speed";
|
2017-04-17 06:19:53 +02:00
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
public static final String RESTORE_QUEUE_INDEX = "restore_queue_index";
|
|
|
|
public static final String RESTORE_WINDOW_POS = "restore_window_pos";
|
|
|
|
|
2017-08-31 19:07:18 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
2017-09-14 17:44:09 +02:00
|
|
|
// Playback
|
2017-08-31 19:07:18 +02:00
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
2017-09-20 06:46:16 +02:00
|
|
|
protected MediaSourceManager playbackManager;
|
2017-08-31 19:07:18 +02:00
|
|
|
protected PlayQueue playQueue;
|
|
|
|
|
2017-09-11 02:43:21 +02:00
|
|
|
private boolean isRecovery = false;
|
|
|
|
private int queuePos = 0;
|
|
|
|
private long videoPos = -1;
|
|
|
|
|
|
|
|
protected StreamInfo currentInfo;
|
2017-09-05 00:38:58 +02:00
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Player
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
public int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds
|
2017-09-18 05:14:02 +02:00
|
|
|
public int PLAY_PREV_ACTIVATION_LIMIT = 5000; // 5 seconds
|
2017-04-17 06:19:53 +02:00
|
|
|
public static final String CACHE_FOLDER_NAME = "exoplayer";
|
|
|
|
|
|
|
|
protected SimpleExoPlayer simpleExoPlayer;
|
|
|
|
protected boolean isPrepared = false;
|
|
|
|
|
2017-09-24 06:50:32 +02:00
|
|
|
protected DefaultTrackSelector trackSelector;
|
2017-04-17 06:19:53 +02:00
|
|
|
protected CacheDataSourceFactory cacheDataSourceFactory;
|
|
|
|
protected final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
|
|
|
|
protected final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
|
|
|
|
2017-09-08 06:47:43 +02:00
|
|
|
protected int PROGRESS_LOOP_INTERVAL = 500;
|
2017-09-07 22:01:02 +02:00
|
|
|
protected Disposable progressUpdateReactor;
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
public BasePlayer(Context context) {
|
|
|
|
this.context = context;
|
|
|
|
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
|
|
|
|
|
|
|
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) {
|
2017-09-03 08:04:18 +02:00
|
|
|
DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Downloader.USER_AGENT, bandwidthMeter);
|
2017-04-17 06:19:53 +02:00
|
|
|
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();
|
|
|
|
|
2017-10-08 15:23:05 +02:00
|
|
|
if (audioManager == null) {
|
|
|
|
this.audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
|
|
|
|
}
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
2017-09-24 06:50:32 +02:00
|
|
|
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
DefaultLoadControl loadControl = new DefaultLoadControl();
|
|
|
|
|
2017-08-18 20:07:57 +02:00
|
|
|
final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
|
2017-09-24 06:50:32 +02:00
|
|
|
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
|
|
|
|
simpleExoPlayer.setAudioDebugListener(this);
|
2017-04-17 06:19:53 +02:00
|
|
|
simpleExoPlayer.addListener(this);
|
2017-09-14 17:44:09 +02:00
|
|
|
simpleExoPlayer.setPlayWhenReady(true);
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
2017-09-07 22:01:02 +02:00
|
|
|
public void initListeners() {}
|
|
|
|
|
|
|
|
protected Disposable getProgressReactor() {
|
|
|
|
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.filter(new Predicate<Long>() {
|
|
|
|
@Override
|
|
|
|
public boolean test(@NonNull Long aLong) throws Exception {
|
|
|
|
return isProgressLoopRunning();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.subscribe(new Consumer<Long>() {
|
|
|
|
@Override
|
|
|
|
public void accept(Long aLong) throws Exception {
|
|
|
|
triggerProgressUpdate();
|
|
|
|
}
|
|
|
|
});
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void handleIntent(Intent intent) {
|
|
|
|
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
|
|
|
if (intent == null) return;
|
|
|
|
|
2017-09-11 02:43:21 +02:00
|
|
|
setRecovery(
|
|
|
|
intent.getIntExtra(RESTORE_QUEUE_INDEX, 0),
|
|
|
|
intent.getLongExtra(START_POSITION, 0)
|
|
|
|
);
|
2017-06-26 04:41:52 +02:00
|
|
|
setPlaybackSpeed(intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()));
|
2017-04-17 06:19:53 +02:00
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
switch (intent.getStringExtra(INTENT_TYPE)) {
|
|
|
|
case SINGLE_STREAM:
|
|
|
|
handleSinglePlaylistIntent(intent);
|
|
|
|
break;
|
|
|
|
case EXTERNAL_PLAYLIST:
|
|
|
|
handleExternalPlaylistIntent(intent);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
|
public void handleExternalPlaylistIntent(Intent intent) {
|
|
|
|
final int serviceId = intent.getIntExtra(ExternalPlayQueue.SERVICE_ID, -1);
|
|
|
|
final int index = intent.getIntExtra(ExternalPlayQueue.INDEX, 0);
|
|
|
|
final Serializable serializable = intent.getSerializableExtra(ExternalPlayQueue.STREAMS);
|
2017-09-15 05:16:09 +02:00
|
|
|
final String url = intent.getStringExtra(ExternalPlayQueue.URL);
|
2017-09-05 21:27:12 +02:00
|
|
|
final String nextPageUrl = intent.getStringExtra(ExternalPlayQueue.NEXT_PAGE_URL);
|
|
|
|
|
|
|
|
List<InfoItem> info = new ArrayList<>();
|
|
|
|
if (serializable instanceof List) {
|
|
|
|
for (final Object o : (List) serializable) {
|
|
|
|
if (o instanceof InfoItem) info.add((StreamInfoItem) o);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-15 05:16:09 +02:00
|
|
|
final PlayQueue queue = new ExternalPlayQueue(serviceId, url, nextPageUrl, info, index);
|
2017-09-14 17:44:09 +02:00
|
|
|
initPlayback(this, queue);
|
2017-09-05 21:27:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
|
public void handleSinglePlaylistIntent(Intent intent) {
|
|
|
|
final Serializable serializable = intent.getSerializableExtra(SinglePlayQueue.STREAM);
|
|
|
|
if (!(serializable instanceof StreamInfo)) return;
|
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
final PlayQueue queue = new SinglePlayQueue((StreamInfo) serializable, PlayQueueItem.DEFAULT_QUALITY);
|
|
|
|
initPlayback(this, queue);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected void initPlayback(@NonNull final PlaybackListener listener, @NonNull final PlayQueue queue) {
|
2017-09-24 22:44:31 +02:00
|
|
|
destroyPlayer();
|
|
|
|
initPlayer();
|
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
if (playQueue != null) playQueue.dispose();
|
|
|
|
if (playbackManager != null) playbackManager.dispose();
|
2017-09-05 21:27:12 +02:00
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
playQueue = queue;
|
|
|
|
playQueue.init();
|
2017-09-20 06:46:16 +02:00
|
|
|
playbackManager = new MediaSourceManager(this, playQueue);
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
2017-09-07 22:01:02 +02:00
|
|
|
public void initThumbnail(final String url) {
|
2017-09-08 06:47:43 +02:00
|
|
|
if (DEBUG) Log.d(TAG, "initThumbnail() called");
|
|
|
|
if (url == null || url.isEmpty()) return;
|
|
|
|
ImageLoader.getInstance().resume();
|
|
|
|
ImageLoader.getInstance().loadImage(url, new SimpleImageLoadingListener() {
|
2017-04-17 06:19:53 +02:00
|
|
|
@Override
|
2017-09-08 06:47:43 +02:00
|
|
|
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
|
|
|
if (simpleExoPlayer == null) return;
|
|
|
|
if (DEBUG) Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]");
|
|
|
|
onThumbnailReceived(loadedImage);
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
2017-09-08 06:47:43 +02:00
|
|
|
});
|
2017-09-07 22:01:02 +02:00
|
|
|
}
|
|
|
|
|
2017-09-08 06:47:43 +02:00
|
|
|
|
2017-09-07 22:01:02 +02:00
|
|
|
public void onThumbnailReceived(Bitmap thumbnail) {
|
|
|
|
if (DEBUG) Log.d(TAG, "onThumbnailReceived() called with: thumbnail = [" + thumbnail + "]");
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void destroyPlayer() {
|
|
|
|
if (DEBUG) Log.d(TAG, "destroyPlayer() called");
|
|
|
|
if (simpleExoPlayer != null) {
|
|
|
|
simpleExoPlayer.stop();
|
|
|
|
simpleExoPlayer.release();
|
|
|
|
}
|
2017-09-07 22:01:02 +02:00
|
|
|
if (isProgressLoopRunning()) stopProgressLoop();
|
2017-09-03 08:04:18 +02:00
|
|
|
if (audioManager != null) {
|
|
|
|
audioManager.abandonAudioFocus(this);
|
|
|
|
audioManager = null;
|
|
|
|
}
|
2017-09-15 04:52:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void destroy() {
|
|
|
|
if (DEBUG) Log.d(TAG, "destroy() called");
|
|
|
|
destroyPlayer();
|
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
if (playQueue != null) {
|
|
|
|
playQueue.dispose();
|
|
|
|
playQueue = null;
|
|
|
|
}
|
|
|
|
if (playbackManager != null) {
|
|
|
|
playbackManager.dispose();
|
|
|
|
playbackManager = null;
|
|
|
|
}
|
2017-04-17 06:19:53 +02:00
|
|
|
|
|
|
|
unregisterBroadcastReceiver();
|
2017-09-07 22:01:02 +02:00
|
|
|
|
2017-09-24 06:50:32 +02:00
|
|
|
trackSelector = null;
|
2017-04-17 06:19:53 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-07 15:04:36 +02:00
|
|
|
private boolean isResumeAfterAudioFocusGain() {
|
2017-09-03 08:04:18 +02:00
|
|
|
return sharedPreferences != null && context != null
|
|
|
|
&& sharedPreferences.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), false);
|
2017-08-07 15:04:36 +02:00
|
|
|
}
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
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);
|
2017-08-07 15:04:36 +02:00
|
|
|
|
2017-09-18 07:47:29 +02:00
|
|
|
if (isResumeAfterAudioFocusGain()) {
|
|
|
|
simpleExoPlayer.setPlayWhenReady(true);
|
|
|
|
}
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-09-24 06:50:32 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Audio Processing
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAudioEnabled(DecoderCounters decoderCounters) {}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAudioSessionId(int i) {
|
|
|
|
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
|
|
|
|
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, i);
|
|
|
|
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
|
|
|
|
context.sendBroadcast(intent);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAudioDecoderInitialized(String s, long l, long l1) {}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAudioInputFormatChanged(Format format) {}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAudioTrackUnderrun(int i, long l, long l1) {}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAudioDisabled(DecoderCounters decoderCounters) {}
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// States Implementation
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
public static final int STATE_BLOCKED = 123;
|
2017-04-17 06:19:53 +02:00
|
|
|
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) {
|
2017-09-14 17:44:09 +02:00
|
|
|
case STATE_BLOCKED:
|
|
|
|
onBlocked();
|
2017-04-17 06:19:53 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
public void onBlocked() {
|
|
|
|
if (DEBUG) Log.d(TAG, "onBlocked() called");
|
2017-09-07 22:01:02 +02:00
|
|
|
if (!isProgressLoopRunning()) startProgressLoop();
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void onPlaying() {
|
|
|
|
if (DEBUG) Log.d(TAG, "onPlaying() called");
|
2017-09-07 22:01:02 +02:00
|
|
|
if (!isProgressLoopRunning()) startProgressLoop();
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void onBuffering() {
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onPaused() {
|
2017-09-07 22:01:02 +02:00
|
|
|
if (isProgressLoopRunning()) stopProgressLoop();
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void onPausedSeek() {
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onCompleted() {
|
|
|
|
if (DEBUG) Log.d(TAG, "onCompleted() called");
|
2017-09-14 17:44:09 +02:00
|
|
|
if (playQueue.getIndex() < playQueue.size() - 1) playQueue.offsetIndex(+1);
|
2017-09-07 22:01:02 +02:00
|
|
|
if (isProgressLoopRunning()) stopProgressLoop();
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Repeat
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
public void onRepeatClicked() {
|
|
|
|
if (DEBUG) Log.d(TAG, "onRepeatClicked() called");
|
|
|
|
|
2017-09-05 00:38:58 +02:00
|
|
|
final int mode;
|
2017-04-17 06:19:53 +02:00
|
|
|
|
2017-09-05 00:38:58 +02:00
|
|
|
switch (simpleExoPlayer.getRepeatMode()) {
|
|
|
|
case Player.REPEAT_MODE_OFF:
|
|
|
|
mode = Player.REPEAT_MODE_ONE;
|
|
|
|
break;
|
|
|
|
case Player.REPEAT_MODE_ONE:
|
|
|
|
mode = Player.REPEAT_MODE_ALL;
|
|
|
|
break;
|
|
|
|
case Player.REPEAT_MODE_ALL:
|
|
|
|
default:
|
|
|
|
mode = Player.REPEAT_MODE_OFF;
|
|
|
|
break;
|
|
|
|
}
|
2017-09-05 21:27:12 +02:00
|
|
|
|
2017-09-05 00:38:58 +02:00
|
|
|
simpleExoPlayer.setRepeatMode(mode);
|
|
|
|
if (DEBUG) Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + simpleExoPlayer.getRepeatMode());
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
2017-09-14 20:02:18 +02:00
|
|
|
// Timeline
|
2017-04-17 06:19:53 +02:00
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
2017-09-14 20:02:18 +02:00
|
|
|
private void refreshTimeline() {
|
2017-09-24 02:02:05 +02:00
|
|
|
playbackManager.load();
|
|
|
|
|
2017-09-11 02:43:21 +02:00
|
|
|
final int currentSourceIndex = playbackManager.getCurrentSourceIndex();
|
|
|
|
|
2017-09-14 20:02:18 +02:00
|
|
|
// Sanity checks
|
2017-09-14 17:44:09 +02:00
|
|
|
if (currentSourceIndex < 0) return;
|
|
|
|
|
|
|
|
// Check if already playing correct window
|
|
|
|
final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex;
|
2017-09-11 02:43:21 +02:00
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
// Check if on wrong window
|
|
|
|
if (!isCurrentWindowCorrect) {
|
|
|
|
final long startPos = currentInfo != null ? currentInfo.start_position : 0;
|
|
|
|
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos));
|
|
|
|
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
|
|
|
}
|
2017-09-11 02:43:21 +02:00
|
|
|
|
2017-09-14 20:02:18 +02:00
|
|
|
// Check if recovering
|
2017-09-24 02:02:05 +02:00
|
|
|
if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) {
|
|
|
|
// todo: figure out exactly why this is the case
|
|
|
|
/* Rounding time to nearest second as certain media cannot guarantee a sub-second seek
|
|
|
|
will complete and the player might get stuck in buffering state forever */
|
|
|
|
final long roundedPos = (videoPos / 1000) * 1000;
|
|
|
|
|
|
|
|
if (DEBUG) Log.d(TAG, "Rewinding to recovery window: " + currentSourceIndex + " at: " + getTimeString((int)roundedPos));
|
|
|
|
simpleExoPlayer.seekTo(roundedPos);
|
2017-09-11 02:43:21 +02:00
|
|
|
isRecovery = false;
|
2017-09-10 01:11:45 +02:00
|
|
|
}
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
2017-09-14 20:02:18 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// ExoPlayer Listener
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
|
|
|
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
|
|
|
|
|
|
|
|
refreshTimeline();
|
|
|
|
}
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
@Override
|
|
|
|
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
2017-09-24 06:50:32 +02:00
|
|
|
if (DEBUG) Log.d(TAG, "onTracksChanged(), track group size = " + trackGroups.length);
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
2017-06-26 04:41:52 +02:00
|
|
|
@Override
|
|
|
|
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
2017-09-07 22:01:02 +02:00
|
|
|
if (DEBUG) Log.d(TAG, "playbackParameters(), speed: " + playbackParameters.speed + ", pitch: " + playbackParameters.pitch);
|
2017-06-26 04:41:52 +02:00
|
|
|
}
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
@Override
|
|
|
|
public void onLoadingChanged(boolean isLoading) {
|
|
|
|
if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]");
|
|
|
|
|
2017-09-07 22:01:02 +02:00
|
|
|
if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) stopProgressLoop();
|
|
|
|
else if (isLoading && !isProgressLoopRunning()) startProgressLoop();
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
2017-09-03 08:04:18 +02:00
|
|
|
if (DEBUG)
|
|
|
|
Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]");
|
2017-09-16 06:28:56 +02:00
|
|
|
if (getCurrentState() == STATE_PAUSED_SEEK) {
|
2017-09-14 17:44:09 +02:00
|
|
|
if (DEBUG) Log.d(TAG, "onPlayerStateChanged() is currently blocked");
|
2017-04-17 06:19:53 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (playbackState) {
|
2017-08-18 20:07:57 +02:00
|
|
|
case Player.STATE_IDLE: // 1
|
2017-04-17 06:19:53 +02:00
|
|
|
isPrepared = false;
|
|
|
|
break;
|
2017-08-18 20:07:57 +02:00
|
|
|
case Player.STATE_BUFFERING: // 2
|
2017-09-24 02:02:05 +02:00
|
|
|
if (isPrepared) {
|
|
|
|
changeState(STATE_BUFFERING);
|
|
|
|
}
|
2017-04-17 06:19:53 +02:00
|
|
|
break;
|
2017-08-18 20:07:57 +02:00
|
|
|
case Player.STATE_READY: //3
|
2017-04-17 06:19:53 +02:00
|
|
|
if (!isPrepared) {
|
|
|
|
isPrepared = true;
|
|
|
|
onPrepared(playWhenReady);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (currentState == STATE_PAUSED_SEEK) break;
|
|
|
|
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
|
|
|
|
break;
|
2017-08-18 20:07:57 +02:00
|
|
|
case Player.STATE_ENDED: // 4
|
2017-09-16 06:28:56 +02:00
|
|
|
// Ensure the current window has actually ended
|
|
|
|
// since single windows that are still loading may produce an ended state
|
|
|
|
if (simpleExoPlayer.isCurrentWindowSeekable() &&
|
|
|
|
simpleExoPlayer.getCurrentPosition() >= simpleExoPlayer.getDuration()) {
|
2017-09-14 17:44:09 +02:00
|
|
|
changeState(STATE_COMPLETED);
|
|
|
|
isPrepared = false;
|
2017-09-06 08:49:00 +02:00
|
|
|
}
|
2017-04-17 06:19:53 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPlayerError(ExoPlaybackException error) {
|
|
|
|
if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]");
|
2017-09-11 02:43:21 +02:00
|
|
|
playQueue.remove(playQueue.getIndex());
|
2017-04-17 06:19:53 +02:00
|
|
|
onError(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPositionDiscontinuity() {
|
2017-09-06 02:48:48 +02:00
|
|
|
// Refresh the playback if there is a transition to the next video
|
2017-09-18 05:14:02 +02:00
|
|
|
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
|
|
|
final int newQueueIndex = playbackManager.getQueueIndexOf(newWindowIndex);
|
|
|
|
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " +
|
|
|
|
"window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]");
|
2017-09-06 02:48:48 +02:00
|
|
|
|
2017-09-20 06:46:16 +02:00
|
|
|
// If the user selects a new track, then the discontinuity occurs after the index is changed.
|
|
|
|
// Therefore, the only source that causes a discrepancy would be autoplay,
|
|
|
|
// which can only offset the current track by +1.
|
|
|
|
if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
|
2017-08-31 19:07:18 +02:00
|
|
|
}
|
|
|
|
|
2017-09-07 22:01:02 +02:00
|
|
|
@Override
|
|
|
|
public void onRepeatModeChanged(int i) {
|
|
|
|
if (DEBUG) Log.d(TAG, "onRepeatModeChanged() called with: mode = [" + i + "]");
|
|
|
|
}
|
|
|
|
|
2017-08-31 19:07:18 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Playback Listener
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void block() {
|
2017-09-06 02:48:48 +02:00
|
|
|
if (simpleExoPlayer == null) return;
|
2017-09-06 08:49:00 +02:00
|
|
|
if (DEBUG) Log.d(TAG, "Blocking...");
|
2017-09-03 04:30:34 +02:00
|
|
|
|
2017-09-04 04:15:11 +02:00
|
|
|
simpleExoPlayer.stop();
|
2017-09-11 02:43:21 +02:00
|
|
|
isPrepared = false;
|
2017-09-04 04:15:11 +02:00
|
|
|
|
2017-09-20 06:46:16 +02:00
|
|
|
changeState(STATE_BLOCKED);
|
2017-08-31 19:07:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2017-09-20 06:46:16 +02:00
|
|
|
public void unblock(final MediaSource mediaSource) {
|
2017-09-06 02:48:48 +02:00
|
|
|
if (simpleExoPlayer == null) return;
|
2017-09-06 08:49:00 +02:00
|
|
|
if (DEBUG) Log.d(TAG, "Unblocking...");
|
2017-09-04 04:15:11 +02:00
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
if (getCurrentState() == STATE_BLOCKED) changeState(STATE_BUFFERING);
|
2017-09-20 06:46:16 +02:00
|
|
|
|
|
|
|
simpleExoPlayer.prepare(mediaSource);
|
2017-09-02 20:06:36 +02:00
|
|
|
}
|
|
|
|
|
2017-09-01 02:47:56 +02:00
|
|
|
@Override
|
2017-09-24 22:44:31 +02:00
|
|
|
public void sync(@Nullable final StreamInfo info) {
|
2017-09-06 02:48:48 +02:00
|
|
|
if (simpleExoPlayer == null) return;
|
2017-09-06 08:49:00 +02:00
|
|
|
if (DEBUG) Log.d(TAG, "Syncing...");
|
2017-09-04 04:15:11 +02:00
|
|
|
|
2017-09-14 20:02:18 +02:00
|
|
|
refreshTimeline();
|
2017-09-06 02:48:48 +02:00
|
|
|
|
2017-09-24 22:44:31 +02:00
|
|
|
if (info == null) return;
|
|
|
|
|
|
|
|
currentInfo = info;
|
2017-09-11 02:43:21 +02:00
|
|
|
initThumbnail(info.thumbnail_url);
|
2017-09-03 04:30:34 +02:00
|
|
|
}
|
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
@Override
|
|
|
|
public void shutdown() {
|
2017-09-06 08:49:00 +02:00
|
|
|
if (DEBUG) Log.d(TAG, "Shutting down...");
|
2017-09-05 21:27:12 +02:00
|
|
|
|
|
|
|
playbackManager.dispose();
|
|
|
|
playQueue.dispose();
|
|
|
|
destroy();
|
|
|
|
}
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// General Player
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
public abstract void onError(Exception exception);
|
2017-04-17 06:19:53 +02:00
|
|
|
|
|
|
|
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 (!isPlaying()) audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
2017-08-02 19:14:45 +02:00
|
|
|
else audioManager.abandonAudioFocus(this);
|
2017-04-17 06:19:53 +02:00
|
|
|
|
2017-09-15 04:52:40 +02:00
|
|
|
if (getCurrentState() == STATE_COMPLETED) {
|
|
|
|
if (playQueue.getIndex() == 0) simpleExoPlayer.seekToDefaultPosition();
|
|
|
|
else playQueue.setIndex(0);
|
|
|
|
}
|
2017-04-17 06:19:53 +02:00
|
|
|
simpleExoPlayer.setPlayWhenReady(!isPlaying());
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-09-18 05:14:02 +02:00
|
|
|
public void onPlayPrevious() {
|
|
|
|
if (simpleExoPlayer == null || playQueue == null || currentInfo == null) return;
|
|
|
|
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
|
|
|
|
|
2017-09-18 07:47:29 +02:00
|
|
|
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track.
|
|
|
|
* Also restart the track if the current track is the first in a queue.*/
|
|
|
|
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) {
|
2017-09-18 05:14:02 +02:00
|
|
|
simpleExoPlayer.seekTo(currentInfo.start_position);
|
2017-09-18 07:47:29 +02:00
|
|
|
} else {
|
|
|
|
playQueue.offsetIndex(-1);
|
2017-09-18 05:14:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onPlayNext() {
|
|
|
|
if (playQueue == null) return;
|
|
|
|
if (DEBUG) Log.d(TAG, "onPlayNext() called");
|
|
|
|
|
|
|
|
playQueue.offsetIndex(+1);
|
|
|
|
}
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
public void seekBy(int milliSeconds) {
|
|
|
|
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
|
2017-09-03 08:04:18 +02:00
|
|
|
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0)))
|
|
|
|
return;
|
2017-04-17 06:19:53 +02:00
|
|
|
int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds);
|
|
|
|
if (progress < 0) progress = 0;
|
|
|
|
simpleExoPlayer.seekTo(progress);
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isPlaying() {
|
2017-08-18 20:07:57 +02:00
|
|
|
return simpleExoPlayer.getPlaybackState() == Player.STATE_READY && simpleExoPlayer.getPlayWhenReady();
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Utils
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
|
|
|
private final StringBuilder stringBuilder = new StringBuilder();
|
|
|
|
private final Formatter formatter = new Formatter(stringBuilder, Locale.getDefault());
|
2017-06-26 04:41:52 +02:00
|
|
|
private final NumberFormat speedFormatter = new DecimalFormat("0.##x");
|
2017-04-17 06:19:53 +02:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2017-06-26 04:41:52 +02:00
|
|
|
protected String formatSpeed(float speed) {
|
|
|
|
return speedFormatter.format(speed);
|
|
|
|
}
|
|
|
|
|
2017-04-17 06:19:53 +02:00
|
|
|
protected void startProgressLoop() {
|
2017-09-07 22:01:02 +02:00
|
|
|
if (progressUpdateReactor != null) progressUpdateReactor.dispose();
|
|
|
|
progressUpdateReactor = getProgressReactor();
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
protected void stopProgressLoop() {
|
2017-09-07 22:01:02 +02:00
|
|
|
if (progressUpdateReactor != null) progressUpdateReactor.dispose();
|
|
|
|
progressUpdateReactor = null;
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
2017-09-11 02:43:21 +02:00
|
|
|
onUpdateProgress(
|
|
|
|
(int) simpleExoPlayer.getCurrentPosition(),
|
|
|
|
(int) simpleExoPlayer.getDuration(),
|
|
|
|
simpleExoPlayer.getBufferedPercentage()
|
|
|
|
);
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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 int getCurrentState() {
|
|
|
|
return currentState;
|
|
|
|
}
|
|
|
|
|
|
|
|
public String getVideoUrl() {
|
2017-09-11 02:43:21 +02:00
|
|
|
return currentInfo.url;
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
2017-09-11 02:43:21 +02:00
|
|
|
public long getVideoPos() {
|
|
|
|
return videoPos;
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public String getVideoTitle() {
|
2017-09-11 02:43:21 +02:00
|
|
|
return currentInfo.name;
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
2017-09-03 08:04:18 +02:00
|
|
|
public String getUploaderName() {
|
2017-09-11 02:43:21 +02:00
|
|
|
return currentInfo.uploader_name;
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isCompleted() {
|
2017-08-18 20:07:57 +02:00
|
|
|
return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED;
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isPrepared() {
|
|
|
|
return isPrepared;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setPrepared(boolean prepared) {
|
|
|
|
isPrepared = prepared;
|
|
|
|
}
|
|
|
|
|
2017-06-26 04:41:52 +02:00
|
|
|
public float getPlaybackSpeed() {
|
|
|
|
return simpleExoPlayer.getPlaybackParameters().speed;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setPlaybackSpeed(float speed) {
|
|
|
|
simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, 1f));
|
|
|
|
}
|
2017-09-05 21:27:12 +02:00
|
|
|
|
|
|
|
public int getCurrentQueueIndex() {
|
|
|
|
return playQueue != null ? playQueue.getIndex() : -1;
|
|
|
|
}
|
|
|
|
|
2017-09-14 17:44:09 +02:00
|
|
|
public long getPlayerCurrentPosition() {
|
|
|
|
return simpleExoPlayer != null ? simpleExoPlayer.getCurrentPosition() : 0L;
|
|
|
|
}
|
|
|
|
|
2017-09-05 21:27:12 +02:00
|
|
|
public PlayQueue getPlayQueue() {
|
|
|
|
return playQueue;
|
|
|
|
}
|
2017-09-06 02:48:48 +02:00
|
|
|
|
2017-09-06 08:49:00 +02:00
|
|
|
public boolean isPlayerReady() {
|
|
|
|
return currentState == STATE_PLAYING || currentState == STATE_COMPLETED || currentState == STATE_PAUSED;
|
2017-09-06 02:48:48 +02:00
|
|
|
}
|
2017-09-07 22:01:02 +02:00
|
|
|
|
|
|
|
public boolean isProgressLoopRunning() {
|
|
|
|
return progressUpdateReactor != null && !progressUpdateReactor.isDisposed();
|
|
|
|
}
|
2017-09-11 02:43:21 +02:00
|
|
|
|
|
|
|
public boolean getRecovery() {
|
|
|
|
return isRecovery;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setRecovery(final int queuePos, final long windowPos) {
|
|
|
|
if (DEBUG) Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos);
|
|
|
|
this.isRecovery = true;
|
|
|
|
this.queuePos = queuePos;
|
|
|
|
this.videoPos = windowPos;
|
|
|
|
}
|
2017-04-17 06:19:53 +02:00
|
|
|
}
|