diff --git a/app/build.gradle b/app/build.gradle
index 652eeb79212..afc50065e9a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -125,7 +125,7 @@ ext {
androidxWorkVersion = '2.8.1'
stateSaverVersion = '1.4.1'
- exoPlayerVersion = '2.18.7'
+ exoPlayerVersion = '2.19.1'
googleAutoServiceVersion = '1.1.1'
groupieVersion = '2.10.1'
markwonVersion = '4.6.2'
@@ -214,7 +214,7 @@ dependencies {
// the corresponding commit hash, since JitPack sometimes deletes artifacts.
// If there’s already a git hash, just add more of it to the end (or remove a letter)
// to cause jitpack to regenerate the artifact.
- implementation 'com.github.TeamNewPipe:NewPipeExtractor:68b4c9acbae2d167e7b1209bb6bf0ae086dd427e'
+ implementation 'com.github.davidasunmo.NewPipeExtractor:NewPipeExtractor:45df3cdb1013370a5ef9d1f9ca0df8a7cf71ebf2'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index b709c110727..a5537fab73b 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -70,6 +70,8 @@
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
+import org.schabi.newpipe.extractor.utils.ExtractorLogger;
+import org.schabi.newpipe.extractor.utils.Logger;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
@@ -137,6 +139,29 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
if (DEBUG) {
+ // Override Extractor to print to Logcat
+ ExtractorLogger.setLogger(new Logger() {
+ @Override
+ public void debug(final String tag, final String message) {
+ Log.d(tag, message);
+ }
+
+ @Override
+ public void warn(final String tag, final String message) {
+ Log.w(tag, message);
+ }
+
+ @Override
+ public void error(final String tag, final String message) {
+ Log.e(tag, message);
+ }
+
+ @Override
+ public void error(final String tag, final String message, final Throwable t) {
+ Log.e(tag, message, t);
+ }
+
+ });
Log.d(TAG, "onCreate() called with: "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index 040f0dc99dc..b8cdc898606 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -226,8 +226,8 @@ public final class Player implements PlaybackListener, Listener {
// audio only mode does not mean that player type is background, but that the player was
// minimized to background but will resume automatically to the original player type
- private boolean isAudioOnly = false;
- private boolean isPrepared = false;
+ private boolean isAudioOnly;
+ private boolean isPrepared;
/*//////////////////////////////////////////////////////////////////////////
// UIs, listeners and disposables
@@ -239,9 +239,9 @@ public final class Player implements PlaybackListener, Listener {
private BroadcastReceiver broadcastReceiver;
private IntentFilter intentFilter;
@Nullable
- private PlayerServiceEventListener fragmentListener = null;
+ private PlayerServiceEventListener fragmentListener;
@Nullable
- private PlayerEventListener activityListener = null;
+ private PlayerEventListener activityListener;
@NonNull
private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
@@ -386,7 +386,7 @@ public void handleIntent(@NonNull final Intent intent) {
final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString(
R.string.playback_skip_silence_key), getPlaybackSkipSilence());
- final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue);
+ final boolean samePlayQueue = newQueue.equalStreamsAndIndex(playQueue);
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted());
@@ -636,7 +636,9 @@ private void setRecovery(final int queuePos, final long windowPos) {
}
if (DEBUG) {
- Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos);
+ final var currentTitle = currentItem != null ? currentItem.getTitle() : "";
+ Log.d(TAG, "Setting recovery, queue: "
+ + queuePos + "[" + currentTitle + "], pos: " + windowPos);
}
playQueue.setRecovery(queuePos, windowPos);
}
@@ -951,6 +953,34 @@ private Disposable getProgressUpdateDisposable() {
//endregion
+ public static String exoplayerStateToString(final int playbackState) {
+ return switch (playbackState) {
+ case com.google.android.exoplayer2.Player.STATE_IDLE -> // 1
+ "STATE_IDLE";
+ case com.google.android.exoplayer2.Player.STATE_BUFFERING -> // 2
+ "STATE_BUFFERING";
+ case com.google.android.exoplayer2.Player.STATE_READY -> //3
+ "STATE_READY";
+ case com.google.android.exoplayer2.Player.STATE_ENDED -> // 4
+ "STATE_ENDED";
+ default ->
+ throw new IllegalArgumentException("Unknown playback state " + playbackState);
+ };
+ }
+
+ public static String stateToString(final int state) {
+ return switch (state) {
+ case STATE_PREFLIGHT -> "STATE_PREFLIGHT";
+ case STATE_BLOCKED -> "STATE_BLOCKED";
+ case STATE_PLAYING -> "STATE_PLAYING";
+ case STATE_BUFFERING -> "STATE_BUFFERING";
+ case STATE_PAUSED -> "STATE_PAUSED";
+ case STATE_PAUSED_SEEK -> "STATE_PAUSED_SEEK";
+ case STATE_COMPLETED -> "STATE_COMPLETED";
+ default -> throw new IllegalArgumentException("Unknown playback state " + state);
+ };
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// Playback states
@@ -973,7 +1003,7 @@ public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason
public void onPlaybackStateChanged(final int playbackState) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPlaybackStateChanged() called with: "
- + "playbackState = [" + playbackState + "]");
+ + "playbackState = [" + exoplayerStateToString(playbackState) + "]");
}
updatePlaybackState(getPlayWhenReady(), playbackState);
}
@@ -982,7 +1012,7 @@ private void updatePlaybackState(final boolean playWhenReady, final int playback
if (DEBUG) {
Log.d(TAG, "ExoPlayer - updatePlaybackState() called with: "
+ "playWhenReady = [" + playWhenReady + "], "
- + "playbackState = [" + playbackState + "]");
+ + "playbackState = [" + exoplayerStateToString(playbackState) + "]");
}
if (currentState == STATE_PAUSED_SEEK) {
@@ -1060,7 +1090,8 @@ public void onPlaybackUnblock(final MediaSource mediaSource) {
public void changeState(final int state) {
if (DEBUG) {
- Log.d(TAG, "changeState() called with: state = [" + state + "]");
+ Log.d(TAG,
+ "changeState() called with: state = [" + stateToString(state) + "]");
}
currentState = state;
switch (state) {
@@ -1770,7 +1801,7 @@ private void saveStreamProgressState(final long progressMillis) {
.observeOn(AndroidSchedulers.mainThread())
.doOnError(e -> {
if (DEBUG) {
- e.printStackTrace();
+ Log.e(TAG, "Error saving stream state", e);
}
})
.onErrorComplete()
diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/LoggingHttpDataSource.java b/app/src/main/java/org/schabi/newpipe/player/datasource/LoggingHttpDataSource.java
new file mode 100644
index 00000000000..c5ffe87330a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/datasource/LoggingHttpDataSource.java
@@ -0,0 +1,124 @@
+package org.schabi.newpipe.player.datasource;
+
+import static org.schabi.newpipe.MainActivity.DEBUG;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.TransferListener;
+
+import org.schabi.newpipe.DownloaderImpl;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+public class LoggingHttpDataSource extends DefaultHttpDataSource {
+
+ public final String TAG = getClass().getSimpleName() + "@" + hashCode();
+
+ public LoggingHttpDataSource() { }
+
+ public LoggingHttpDataSource(@Nullable final String userAgent,
+ final int connectTimeoutMillis,
+ final int readTimeoutMillis,
+ final boolean allowCrossProtocolRedirects,
+ @Nullable final RequestProperties defaultRequestProperties) {
+ super(userAgent,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ allowCrossProtocolRedirects,
+ defaultRequestProperties);
+ }
+
+
+ @Override
+ public long open(final DataSpec dataSpec) throws HttpDataSourceException {
+ if (!DEBUG) {
+ return super.open(dataSpec);
+ }
+
+ Log.d(TAG, "Request URL: " + dataSpec.uri);
+ try {
+ return super.open(dataSpec);
+ } catch (final HttpDataSource.InvalidResponseCodeException e) {
+ Log.e(TAG, "HTTP error for URL: " + dataSpec.uri);
+ Log.e(TAG, "Response code: " + e.responseCode);
+ Log.e(TAG, "Headers: " + e.headerFields);
+ Log.e(TAG, "Body: " + new String(e.responseBody, StandardCharsets.UTF_8));
+ throw e;
+ }
+ }
+
+ @SuppressWarnings("checkstyle:hiddenField")
+ public static class Factory implements HttpDataSource.Factory {
+
+ final RequestProperties defaultRequestProperties;
+
+ @Nullable
+ TransferListener transferListener;
+ @Nullable
+ String userAgent;
+ int connectTimeoutMs;
+ int readTimeoutMs;
+ boolean allowCrossProtocolRedirects;
+
+ public Factory() {
+ defaultRequestProperties = new RequestProperties();
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
+ readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
+ userAgent = DownloaderImpl.USER_AGENT;
+ }
+
+ @NonNull
+ @Override
+ public HttpDataSource createDataSource() {
+ final var dataSource = new LoggingHttpDataSource(userAgent,
+ connectTimeoutMs,
+ readTimeoutMs,
+ allowCrossProtocolRedirects,
+ defaultRequestProperties);
+ if (transferListener != null) {
+ dataSource.addTransferListener(transferListener);
+ }
+ return dataSource;
+ }
+
+ @NonNull
+ @Override
+ public Factory setDefaultRequestProperties(
+ @NonNull final Map defaultRequestProperties) {
+ this.defaultRequestProperties.clearAndSet(defaultRequestProperties);
+ return this;
+ }
+
+ public Factory setUserAgent(@Nullable final String userAgent) {
+ this.userAgent = userAgent;
+ return this;
+ }
+
+ public Factory setConnectTimeoutMs(final int connectTimeoutMs) {
+ this.connectTimeoutMs = connectTimeoutMs;
+ return this;
+ }
+
+ public Factory setReadTimeoutMs(final int readTimeoutMs) {
+ this.readTimeoutMs = readTimeoutMs;
+ return this;
+ }
+
+ public Factory setAllowCrossProtocolRedirects(final boolean allowCrossProtocolRedirects) {
+ this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+ return this;
+ }
+
+ public Factory setTransferListener(@Nullable final TransferListener transferListener) {
+ this.transferListener = transferListener;
+ return this;
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java b/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java
index 676443a9c78..90ed3af1f4d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java
+++ b/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java
@@ -112,7 +112,7 @@ private NonUriHlsDataSourceFactory(@NonNull final DataSource.Factory dataSourceF
*
*
*
- * This change allow playback of non-URI HLS contents, when the manifest is not a master
+ * This change allows playback of non-URI HLS contents, when the manifest is not a master
* manifest/playlist (otherwise, endless loops should be encountered because the
* {@link DataSource}s created for media playlists should use the master playlist response
* instead).
diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/RefreshableHlsHttpDataSource.java b/app/src/main/java/org/schabi/newpipe/player/datasource/RefreshableHlsHttpDataSource.java
new file mode 100644
index 00000000000..929c577c0f4
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/datasource/RefreshableHlsHttpDataSource.java
@@ -0,0 +1,248 @@
+package org.schabi.newpipe.player.datasource;
+
+import static org.schabi.newpipe.MainActivity.DEBUG;
+
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
+import com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+
+import org.schabi.newpipe.extractor.exceptions.ExtractionException;
+import org.schabi.newpipe.extractor.stream.RefreshableStream;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class RefreshableHlsHttpDataSource extends LoggingHttpDataSource {
+
+ private final String TAG =
+ RefreshableHlsHttpDataSource.class.getSimpleName() + "@" + hashCode();
+ private final RefreshableStream refreshableStream;
+ private final String originalPlaylistUrl;
+ private final Map chunkUrlMap = new LinkedHashMap<>();
+ private boolean isError;
+
+ public RefreshableHlsHttpDataSource(final RefreshableStream refreshableStream) {
+ this.refreshableStream = refreshableStream;
+ originalPlaylistUrl = refreshableStream.initialUrl();
+ }
+
+ @SuppressWarnings("checkstyle:LineLength")
+ public RefreshableHlsHttpDataSource(final RefreshableStream refreshableStream,
+ @Nullable final String userAgent,
+ final int connectTimeoutMillis,
+ final int readTimeoutMillis,
+ final boolean allowCrossProtocolRedirects,
+ @Nullable final RequestProperties defaultRequestProperties) {
+ super(userAgent,
+ connectTimeoutMillis,
+ readTimeoutMillis,
+ allowCrossProtocolRedirects,
+ defaultRequestProperties);
+ this.refreshableStream = refreshableStream;
+ originalPlaylistUrl = refreshableStream.initialUrl();
+ }
+
+ @Override
+ public long open(final DataSpec dataSpec) throws HttpDataSourceException {
+ final var url = dataSpec.uri.toString();
+ if (DEBUG) {
+ Log.d(TAG, "called open(" + url + ")");
+ }
+
+ if (!url.contains(refreshableStream.playlistId())) {
+ // TODO: throw error or no?
+ if (DEBUG) {
+ Log.e(TAG, "Playlist id does not match");
+ }
+ }
+ return chunkUrlMap.isEmpty()
+ ? openInternal(dataSpec)
+ : openInternal(getUpdatedDataSpec(dataSpec));
+ }
+
+ private long openInternal(final DataSpec dataSpec) throws HttpDataSourceException {
+ try {
+ final var bytesToRead = super.open(dataSpec);
+ if (DEBUG) {
+ Log.d(TAG, "Bytes to read: " + bytesToRead);
+ }
+ isError = false; // if we got to this line there was no error
+ return bytesToRead;
+ } catch (final InvalidResponseCodeException e) {
+ // TODO: This assumes SoundCloud returning 403 when playlist expires
+ // If we need to refresh playlists for other services at a later date then
+ // need to generalize this class
+ if (isError || e.responseCode != 403) {
+ // Use isError to prevent infinite loop if playlist expires, we replace signature
+ // but then that one gives an error, and then we replace signature again, and so on
+ // The expectation is that no error will be thrown on the first recursion
+ throw e;
+ }
+
+ try {
+ refreshPlaylist();
+ } catch (final ExtractionException | IOException ex) {
+ throw new HttpDataSourceException("Error refreshing Hls playlist: "
+ + originalPlaylistUrl,
+ new IOException(ex), dataSpec,
+ HttpDataSourceException.TYPE_OPEN);
+ }
+ isError = true;
+ // Use recursion to reuse error handling without code duplication
+ return openInternal(getUpdatedDataSpec(dataSpec));
+ }
+ }
+
+ private void refreshPlaylist() throws ExtractionException, IOException {
+ if (DEBUG) {
+ Log.d(TAG, "refreshPlaylist() - originalPlaylistUrl " + originalPlaylistUrl);
+ }
+
+ final var newPlaylistUrl = refreshableStream.fetchLatestUrl();
+
+ if (DEBUG) {
+ Log.d(TAG, "New playlist url " + newPlaylistUrl);
+ Log.d(TAG, "Extracting new playlist Chunks");
+ }
+ final var newChunks = extractChunksFromPlaylist(newPlaylistUrl);
+
+ if (!chunkUrlMap.isEmpty()) {
+ updateChunkMap(chunkUrlMap, newChunks);
+ }
+ initializeChunkMappings(chunkUrlMap, newChunks);
+ }
+
+ private static void initializeChunkMappings(final Map chunkMap,
+ final List newChunks) {
+ for (int i = 0; i < newChunks.size(); ++i) {
+ final var newUrl = newChunks.get(i);
+ chunkMap.put(removeQueryParameters(newUrl), newUrl);
+ }
+ }
+
+ private static void updateChunkMap(final Map chunkMap,
+ final List newChunks) throws IOException {
+ if (chunkMap.size() != newChunks.size()) {
+ throw new IOException("Error extracting chunks: chunks are not same size\n"
+ + "Expected " + chunkMap.size()
+ + " and got " + newChunks.size());
+ }
+
+ final var baseUrlIt = chunkMap.keySet().iterator();
+ final var newChunkUrlIt = newChunks.iterator();
+ while (baseUrlIt.hasNext()) {
+ chunkMap.put(baseUrlIt.next(), newChunkUrlIt.next());
+ }
+ }
+
+ private static String removeQueryParameters(final String url) {
+ final int idx = url.indexOf('?');
+ return idx == -1 ? url : url.substring(0, idx);
+ }
+
+ private DataSpec getUpdatedDataSpec(final DataSpec dataSpec) {
+ final var currentUrl = dataSpec.uri.toString();
+ if (DEBUG) {
+ Log.d(TAG, "getUpdatedDataSpec(" + currentUrl + ')');
+ }
+
+ final var baseUrl = removeQueryParameters(currentUrl);
+
+ if (baseUrl.equals(currentUrl)) {
+ if (DEBUG) {
+ Log.e(TAG, "Url has no query parameters");
+ }
+ }
+
+ final var updatedUrl = chunkUrlMap.get(baseUrl);
+ if (updatedUrl == null) {
+ throw new IllegalStateException("baseUrl not found in mappings: " + baseUrl);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "updated url:" + updatedUrl);
+ }
+ return dataSpec.buildUpon()
+ .setUri(Uri.parse(updatedUrl))
+ .build();
+ }
+
+ /**
+ * Extracts the chunks/segments from an m3u8 playlist using
+ * ExoPlayer's {@link HlsPlaylistParser}.
+ * @param playlistUrl url of m3u8 playlist to extract
+ * @return Urls for all the chunks/segments in the playlist
+ * @throws IOException If error extracting the chunks
+ */
+ private List extractChunksFromPlaylist(final String playlistUrl)
+ throws IOException {
+ if (DEBUG) {
+ Log.d(TAG, "extractChunksFromPlaylist(" + playlistUrl + ')');
+ }
+ final var chunks = new ArrayList();
+ final var parser = new HlsPlaylistParser();
+ final var dataSpec = new DataSpec(Uri.parse(playlistUrl));
+ final var httpDataSource = new LoggingHttpDataSource.Factory().createDataSource();
+
+ // Adapted from ParsingLoadable.load()
+ // DataSourceInputStream opens the data source internally on open()
+ // It passes dataSpec to data source
+ // httpDataSource is a DefaultHttpDataSource, and getUri will return dataSpec's uri
+ // which == playlistUrl
+ try (@SuppressWarnings("LocalCanBeFinal")
+ var inputStream = new DataSourceInputStream(httpDataSource, dataSpec)) {
+ inputStream.open();
+
+ final var playlist =
+ parser.parse(Objects.requireNonNull(httpDataSource.getUri()), inputStream);
+
+ if (!(playlist instanceof final HlsMediaPlaylist hlsMediaPlaylist)) {
+ throw new IOException("Expected Hls playlist to be an HlsMediaPlaylist, but was a "
+ + playlist.getClass().getSimpleName());
+ }
+
+ for (final var segment : hlsMediaPlaylist.segments) {
+ chunks.add(segment.url);
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Extracted " + chunks.size() + " chunks");
+ chunks.stream().forEach(m -> Log.d(TAG, "Chunk " + m));
+ }
+
+ return chunks;
+ } finally {
+ httpDataSource.close();
+ }
+ }
+
+ public static class Factory extends LoggingHttpDataSource.Factory {
+ private final RefreshableStream refreshableStream;
+
+ public Factory(final RefreshableStream refreshableStream) {
+ this.refreshableStream = refreshableStream;
+ }
+
+ @NonNull
+ @Override
+ public HttpDataSource createDataSource() {
+ return new RefreshableHlsHttpDataSource(refreshableStream,
+ userAgent,
+ connectTimeoutMs,
+ readTimeoutMs,
+ allowCrossProtocolRedirects,
+ defaultRequestProperties);
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index 0530d56e921..ebdea575796 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -5,7 +5,7 @@
import android.content.Context;
import android.util.Log;
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
@@ -18,16 +18,19 @@
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
-import org.schabi.newpipe.DownloaderImpl;
+import org.jetbrains.annotations.Contract;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
+import org.schabi.newpipe.extractor.stream.RefreshableStream;
+import org.schabi.newpipe.extractor.stream.Stream;
+import org.schabi.newpipe.player.datasource.LoggingHttpDataSource;
import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory;
+import org.schabi.newpipe.player.datasource.RefreshableHlsHttpDataSource;
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
import java.io.File;
@@ -78,27 +81,32 @@ public class PlayerDataSource {
private final CacheFactory ytProgressiveDashCacheDataSourceFactory;
+ private final Context context;
+ private final TransferListener transferListener;
+
+
public PlayerDataSource(final Context context,
final TransferListener transferListener) {
-
+ this.context = context;
+ this.transferListener = transferListener;
progressiveLoadIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
// make sure the static cache was created: needed by CacheFactories below
instantiateCacheIfNeeded(context);
- // generic data source factories use DefaultHttpDataSource.Factory
+ // generic data source factories use LoggingHttpDataSource.Factory, which is a wrapper
+ // around DefaultHttpDataSource
cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
- new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT))
+ new LoggingHttpDataSource.Factory())
.setTransferListener(transferListener);
- cacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
- new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT));
+ cacheDataSourceFactory = createCacheDataSourceFactory(new LoggingHttpDataSource.Factory());
// YouTube-specific data source factories use getYoutubeHttpDataSourceFactory()
- ytHlsCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
+ ytHlsCacheDataSourceFactory = createCacheDataSourceFactory(
getYoutubeHttpDataSourceFactory(false, false));
- ytDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
+ ytDashCacheDataSourceFactory = createCacheDataSourceFactory(
getYoutubeHttpDataSourceFactory(true, true));
- ytProgressiveDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache,
+ ytProgressiveDashCacheDataSourceFactory = createCacheDataSourceFactory(
getYoutubeHttpDataSourceFactory(false, true));
// set the maximum size to manifest creators
@@ -108,6 +116,11 @@ public PlayerDataSource(final Context context,
MAX_MANIFEST_CACHE_SIZE);
}
+ @NonNull
+ @Contract(value = "_ -> new", pure = true)
+ private CacheFactory createCacheDataSourceFactory(final DataSource.Factory wrappedFactory) {
+ return new CacheFactory(context, transferListener, cache, wrappedFactory);
+ }
//region Live media source factories
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
@@ -134,12 +147,23 @@ public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
//region Generic media source factories
public HlsMediaSource.Factory getHlsMediaSourceFactory(
- @Nullable final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder) {
- if (hlsDataSourceFactoryBuilder != null) {
- hlsDataSourceFactoryBuilder.setDataSourceFactory(cacheDataSourceFactory);
- return new HlsMediaSource.Factory(hlsDataSourceFactoryBuilder.build());
+ @NonNull final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder) {
+ hlsDataSourceFactoryBuilder.setDataSourceFactory(cacheDataSourceFactory);
+ return new HlsMediaSource.Factory(hlsDataSourceFactoryBuilder.build());
+ }
+
+ public HlsMediaSource.Factory getHlsMediaSourceFactory(final Stream stream) {
+ if (stream instanceof final RefreshableStream refreshableStream) {
+ return new HlsMediaSource.Factory(
+ createCacheDataSourceFactory(
+ new RefreshableHlsHttpDataSource.Factory(refreshableStream)
+ )
+ );
}
+ return getHlsMediaSourceFactory();
+ }
+ public HlsMediaSource.Factory getHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cacheDataSourceFactory);
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java
index fe884834bc3..abc3c1844ad 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java
@@ -36,7 +36,7 @@
public class MediaSessionPlayerUi extends PlayerUi
implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = "MediaSessUi";
+ private static final String TAG = MediaSessionPlayerUi.class.getSimpleName();
@NonNull
private final MediaSessionCompat mediaSession;
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
index 88d7145bceb..67eec757cee 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java
@@ -9,6 +9,7 @@
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
+import org.schabi.newpipe.extractor.StreamingServiceId;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
@@ -114,6 +115,9 @@ public class MediaSourceManager {
@NonNull
private final CompositeDisposable loaderReactor;
+ /**
+ * The items that are currently being loaded.
+ */
@NonNull
private final Set loadingItems;
@@ -146,6 +150,20 @@ private MediaSourceManager(@NonNull final PlaybackListener listener,
+ " ms] for them to be useful.");
}
+ if (DEBUG) {
+ final var currentItem = playQueue.getItem();
+
+ if (currentItem != null) {
+ Log.d(TAG, "Creating MediaSourceManager["
+ + StreamingServiceId.nameFromId(currentItem.getServiceId())
+ + " currentIndex=" + playQueue.getIndex()
+ + " currentTitle=" + currentItem.getTitle());
+ } else {
+ Log.d(TAG,
+ "Creating MediaSourceManager[currentIndex=" + playQueue.getIndex() + "]");
+ }
+ }
+
this.playbackListener = listener;
this.playQueue = playQueue;
@@ -180,13 +198,15 @@ private MediaSourceManager(@NonNull final PlaybackListener listener,
*/
public void dispose() {
if (DEBUG) {
- Log.d(TAG, "close() called.");
+ Log.d(TAG, "dispose() called.");
}
debouncedSignal.onComplete();
debouncedLoader.dispose();
playQueueReactor.cancel();
+ //TODO: Why not clear here?
+ //TODO: Also why not clear loadingItems here?
loaderReactor.dispose();
}
@@ -483,7 +503,7 @@ private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
* readiness or playlist desynchronization.
*
* If the given {@link PlayQueueItem} is currently being played and is already loaded,
- * then correction is not only needed if the playlist is desynchronized. Otherwise, the
+ * then correction is only needed if the playlist is desynchronized. Otherwise, the
* check depends on the status (e.g. expiration or placeholder) of the
* {@link ManagedMediaSource}.
*
@@ -530,10 +550,12 @@ private void maybeRenewCurrentIndex() {
}
private void maybeClearLoaders() {
+ final var currentItem = playQueue.getItem();
if (DEBUG) {
- Log.d(TAG, "MediaSource - maybeClearLoaders() called.");
+ final var url = currentItem != null ? currentItem.getUrl() : "";
+ Log.d(TAG, "MediaSource - maybeClearLoaders() called. currentItem: " + url);
}
- if (!loadingItems.contains(playQueue.getItem())
+ if (!loadingItems.contains(currentItem)
&& loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
loaderReactor.clear();
loadingItems.clear();
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
index cfa2ab3162c..1cb2a25eb72 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
@@ -538,11 +538,8 @@ public boolean equalStreams(@Nullable final PlayQueue other) {
}
public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) {
- if (equalStreams(other)) {
- //noinspection ConstantConditions
- return other.getIndex() == getIndex(); //NOSONAR: other is not null
- }
- return false;
+ //noinspection ConstantConditions
+ return equalStreams(other) && other.getIndex() == getIndex(); //NOSONAR: other is not null
}
public boolean isDisposed() {
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index e204b8372a7..eff9ae5a23d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -335,7 +335,7 @@ private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSou
throws ResolverException {
if (stream.isUrl()) {
throwResolverExceptionIfUrlNullOrEmpty(stream.getContent());
- return dataSource.getHlsMediaSourceFactory(null).createMediaSource(
+ return dataSource.getHlsMediaSourceFactory(stream).createMediaSource(
new MediaItem.Builder()
.setTag(metadata)
.setUri(Uri.parse(stream.getContent()))
@@ -343,8 +343,7 @@ private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSou
.build());
}
- final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder =
- new NonUriHlsDataSourceFactory.Builder();
+ final var hlsDataSourceFactoryBuilder = new NonUriHlsDataSourceFactory.Builder();
hlsDataSourceFactoryBuilder.setPlaylistString(stream.getContent());
return dataSource.getHlsMediaSourceFactory(hlsDataSourceFactoryBuilder)
diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
index b9c91f8a5b0..e8a91f6ea55 100644
--- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
+++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java
@@ -27,8 +27,11 @@
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.Info;
+import org.schabi.newpipe.extractor.StreamingServiceId;
-import java.util.Map;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
public final class InfoCache {
private final String TAG = getClass().getSimpleName();
@@ -71,23 +74,23 @@ private static String keyOf(final int serviceId,
}
private static void removeStaleCache() {
- for (final Map.Entry entry : InfoCache.LRU_CACHE.snapshot().entrySet()) {
+ for (final var entry : LRU_CACHE.snapshot().entrySet()) {
final CacheData data = entry.getValue();
if (data != null && data.isExpired()) {
- InfoCache.LRU_CACHE.remove(entry.getKey());
+ LRU_CACHE.remove(entry.getKey());
}
}
}
@Nullable
private static Info getInfo(@NonNull final String key) {
- final CacheData data = InfoCache.LRU_CACHE.get(key);
+ final CacheData data = LRU_CACHE.get(key);
if (data == null) {
return null;
}
if (data.isExpired()) {
- InfoCache.LRU_CACHE.remove(key);
+ LRU_CACHE.remove(key);
return null;
}
@@ -100,22 +103,29 @@ public Info getFromKey(final int serviceId,
@NonNull final Type cacheType) {
if (DEBUG) {
Log.d(TAG, "getFromKey() called with: "
- + "serviceId = [" + serviceId + "], url = [" + url + "]");
+ + StreamingServiceId.nameFromId(serviceId) + "[" + url + "]");
}
synchronized (LRU_CACHE) {
return getInfo(keyOf(serviceId, url, cacheType));
}
}
+ @SuppressWarnings("checkStyle:linelength")
public void putInfo(final int serviceId,
@NonNull final String url,
@NonNull final Info info,
@NonNull final Type cacheType) {
+ final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
if (DEBUG) {
- Log.d(TAG, "putInfo() called with: info = [" + info + "]");
+ final var expiryDateInstant = Instant.now().plusMillis(expirationMillis);
+ final var expiryDate = LocalDateTime.ofInstant(expiryDateInstant,
+ ZoneId.systemDefault());
+ Log.d(TAG, "putInfo(): add to cache " + StreamingServiceId.nameFromId(serviceId) + " "
+ + cacheType.name()
+ + " expires on " + expiryDate
+ + " " + info);
}
- final long expirationMillis = ServiceHelper.getCacheExpirationMillis(info.getServiceId());
synchronized (LRU_CACHE) {
final CacheData data = new CacheData(info, expirationMillis);
LRU_CACHE.put(keyOf(serviceId, url, cacheType), data);
diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
index 282a88b1eaf..ebb486a1f5a 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java
@@ -293,7 +293,7 @@ public static boolean isHighResolutionSelected(final String selectedResolution,
public static List getFilteredAudioStreams(
@NonNull final Context context,
@Nullable final List audioStreams) {
- if (audioStreams == null) {
+ if (audioStreams == null || audioStreams.isEmpty()) {
return Collections.emptyList();
}
@@ -302,6 +302,9 @@ public static List getFilteredAudioStreams(
final Comparator cmp = getAudioFormatComparator(context);
for (final AudioStream stream : audioStreams) {
+ // TODO: this doesn't add HLS OPUS streams, but soundcloud has that.
+ // Meaning it never actually plays the OPUS soundcloud streams, only
+ // progressive and hls mp3. So should we change this to allow HLS OPUS?
if (stream.getDeliveryMethod() == DeliveryMethod.TORRENT
|| (stream.getDeliveryMethod() == DeliveryMethod.HLS
&& stream.getFormat() == MediaFormat.OPUS)) {
diff --git a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
index 6e9ea7a47e7..99bf52350cc 100644
--- a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java
@@ -32,7 +32,7 @@ private SparseItemUtil() {
}
/**
- * Use this to certainly obtain an single play queue with all of the data filled in when the
+ * Use this to certainly obtain a single play queue with all of the data filled in when the
* stream info item you are handling might be sparse, e.g. because it was fetched via a {@link
* org.schabi.newpipe.extractor.feed.FeedExtractor}. FeedExtractors provide a fast and
* lightweight method to fetch info, but the info might be incomplete (see
diff --git a/checkstyle/checkstyle.xml b/checkstyle/checkstyle.xml
index ee091fa9f44..ff2554f77de 100644
--- a/checkstyle/checkstyle.xml
+++ b/checkstyle/checkstyle.xml
@@ -70,6 +70,8 @@
+
+
diff --git a/settings.gradle b/settings.gradle
index 0338fde6c55..b878803369a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,6 +6,6 @@ include ':app'
//includeBuild('../NewPipeExtractor') {
// dependencySubstitution {
-// substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor')
+// substitute module('com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor') using project(':extractor')
// }
//}