diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
index 82f7d84bf45..6892c5f4654 100644
--- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
+++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
@@ -258,7 +258,8 @@ class ErrorInfo private constructor(
)
throwable is ContentNotAvailableException ->
- ErrorMessage(R.string.content_not_available)
+ getLiveStreamOfflineMessage(throwable)
+ ? : ErrorMessage(R.string.content_not_available)
// other extractor exceptions
throwable is ContentNotSupportedException ->
@@ -309,7 +310,7 @@ class ErrorInfo private constructor(
// if the service explicitly said that content is not available (e.g. age
// restrictions, video deleted, etc.), there is no use in letting users report it
- is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
+ is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable) && !isLiveStreamOffline(throwable)
// we know the content is not supported, no need to let the user report it
is ContentNotSupportedException -> false
@@ -360,5 +361,26 @@ class ErrorInfo private constructor(
else -> false
}
}
+
+ fun isLiveStreamOffline(e: ContentNotAvailableException): Boolean {
+ return e.message?.contains("LIVE_STREAM_OFFLINE", ignoreCase = true) == true
+ }
+
+ fun getLiveStreamOfflineMessage(e: ContentNotAvailableException): ErrorMessage? {
+ if (!isLiveStreamOffline(e)) return null
+ // YouTube typically embeds the human-readable start hint in quotes, e.g.:
+ // "Got error LIVE_STREAM_OFFLINE: \"This live event will begin in 27 minutes.\""
+ val hint = e.message
+ ?.substringAfter("LIVE_STREAM_OFFLINE:", missingDelimiterValue = "")
+ ?.trim()
+ ?.trim('"')
+ ?.trim()
+ ?.takeIf { it.isNotEmpty() }
+ return if (hint != null) {
+ ErrorMessage(R.string.live_stream_starts_in, hint)
+ } else {
+ ErrorMessage(R.string.live_stream_not_started_yet)
+ }
+ }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index ee93e313846..f1cde29a600 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -75,6 +75,7 @@
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
+import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
@@ -210,6 +211,9 @@ public final class VideoDetailFragment
@Nullable
private StreamInfo currentInfo = null;
private Disposable currentWorker;
+ /** Disposable for the auto-retry timer used when a live stream has not started yet. */
+ @Nullable
+ private Disposable liveStreamRetryDisposable;
@NonNull
private final CompositeDisposable disposables = new CompositeDisposable();
@Nullable
@@ -420,6 +424,7 @@ public void onDestroy() {
if (currentWorker != null) {
currentWorker.dispose();
}
+ cancelLiveStreamAutoRetry();
disposables.clear();
positionSubscriber = null;
currentWorker = null;
@@ -564,10 +569,10 @@ private void setOnLongClickListeners() {
}));
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
- openBackgroundPlayer(true)
+ openBackgroundPlayer(true)
));
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
- openPopupPlayer(true)
+ openPopupPlayer(true)
));
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
NavigationHelper.openDownloads(activity)));
@@ -833,6 +838,7 @@ protected void prepareAndLoadInfo() {
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
+ cancelLiveStreamAutoRetry();
initTabs();
currentInfo = null;
if (currentWorker != null) {
@@ -845,6 +851,7 @@ public void startLoading(final boolean forceLoad) {
private void startLoading(final boolean forceLoad, final boolean addToBackStack) {
super.startLoading(forceLoad);
+ cancelLiveStreamAutoRetry();
initTabs();
currentInfo = null;
if (currentWorker != null) {
@@ -882,8 +889,52 @@ private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
openVideoPlayerAutoFullscreen();
}
}
- }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
- url == null ? "no url" : url, serviceId, url)));
+ }, throwable -> {
+ // If the stream is a scheduled live event not yet started,
+ // show a helpful message with the start hint and a retry button
+ // instead of the generic monkey "Content unavailable" error.
+ if (throwable instanceof ContentNotAvailableException
+ && ErrorInfo.Companion.isLiveStreamOffline(
+ (ContentNotAvailableException) throwable)) {
+ scheduleAutoRetryForLiveStream();
+ }
+ showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
+ url == null ? "no url" : url, serviceId, url));
+ });
+ }
+
+ /**
+ * When a scheduled live stream has not started yet, automatically retry loading
+ * after {@link #LIVE_STREAM_RETRY_DELAY_SECONDS} seconds so the user does not
+ * have to manually tap Retry when the stream eventually goes live.
+ * Any pending retry is cancelled the moment the user navigates away or triggers
+ * a manual reload (disposables are cleared in {@link #onDestroyView}).
+ */
+ private static final long LIVE_STREAM_RETRY_DELAY_SECONDS = 60;
+
+ private void scheduleAutoRetryForLiveStream() {
+ // Cancel any previously scheduled retry before starting a new one
+ cancelLiveStreamAutoRetry();
+ liveStreamRetryDisposable =
+ io.reactivex.rxjava3.core.Completable
+ .timer(LIVE_STREAM_RETRY_DELAY_SECONDS, TimeUnit.SECONDS, Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ () -> {
+ if (isAdded() && !isDetached() && !isRemoving()) {
+ reloadContent();
+ }
+ },
+ throwable -> { /* timer disposed (user navigated away) */ }
+ );
+ disposables.add(liveStreamRetryDisposable);
+ }
+
+ private void cancelLiveStreamAutoRetry() {
+ if (liveStreamRetryDisposable != null && !liveStreamRetryDisposable.isDisposed()) {
+ liveStreamRetryDisposable.dispose();
+ }
+ liveStreamRetryDisposable = null;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -1617,14 +1668,13 @@ public void handleResult(@NonNull final StreamInfo info) {
}
if (!info.getErrors().isEmpty()) {
- // Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is
- // thrown. This is not an error and thus should not be shown to the user.
- for (final Throwable throwable : info.getErrors()) {
- if (throwable instanceof ContentNotSupportedException
- && "Fan pages are not supported".equals(throwable.getMessage())) {
- info.getErrors().remove(throwable);
- }
- }
+ // Bandcamp fan pages are not yet supported and thus a ContentNotSupportedException
+ // is thrown. This is not an error and thus should not be shown to the user.
+ // Use removeIf to avoid ConcurrentModificationException when modifying the list
+ // while iterating over it.
+ info.getErrors().removeIf(throwable ->
+ throwable instanceof ContentNotSupportedException
+ && "Fan pages are not supported".equals(throwable.getMessage()));
if (!info.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(info.getErrors(), UserAction.REQUESTED_STREAM,
@@ -2518,4 +2568,4 @@ private void updateBottomSheetState(final int newState) {
lastStableBottomSheetState = newState;
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 912ad3f3665..8f75e336ee2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -168,7 +168,7 @@
YouTube provides a \"Restricted Mode\" which hides potentially mature content
This video is age restricted.\n\nTurn on \"%1$s\" in the settings if you want to see it.
This video is age-restricted.
-\nDue to new YouTube policies with age-restricted videos, NewPipe cannot access any of its video streams and thus is unable to play it.
+ \nDue to new YouTube policies with age-restricted videos, NewPipe cannot access any of its video streams and thus is unable to play it.
Live
Downloads
Downloads
@@ -238,6 +238,8 @@
Could not load all thumbnails
Could not parse website
Content unavailable
+ This live stream has not started yet
+ This live stream starts in: %s
Could not set up download menu
App/UI crashed
Could not play this stream
@@ -528,14 +530,14 @@
Could not import subscriptions
Could not export subscriptions
Import YouTube subscriptions from Google takeout:
-\n
-\n1. Go to this URL: %1$s
-\n2. Log in when asked
-\n3. Click on \"All data included\", then on \"Deselect all\", then select only \"subscriptions\" and click \"OK\"
-\n4. Click on \"Next step\" and then on \"Create export\"
-\n5. Click on the \"Download\" button after it appears
-\n6. Click on IMPORT FILE below and select the downloaded .zip file
-\n7. [If the .zip import fails] Extract the .csv file (usually under \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), click on IMPORT FILE below and select the extracted csv file
+ \n
+ \n1. Go to this URL: %1$s
+ \n2. Log in when asked
+ \n3. Click on \"All data included\", then on \"Deselect all\", then select only \"subscriptions\" and click \"OK\"
+ \n4. Click on \"Next step\" and then on \"Create export\"
+ \n5. Click on the \"Download\" button after it appears
+ \n6. Click on IMPORT FILE below and select the downloaded .zip file
+ \n7. [If the .zip import fails] Extract the .csv file (usually under \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), click on IMPORT FILE below and select the extracted csv file
Import a SoundCloud profile by typing either the URL or your ID:\n\n1. Enable \"desktop mode\" in a web-browser (the site is not available for mobile devices)\n2. Go to this URL: %1$s\n3. Log in when asked\n4. Copy the profile URL you were redirected to.
yourID, soundcloud.com/yourid
Keep in mind this operation can be network expensive.\n\nDo you want to continue?
@@ -551,7 +553,7 @@
Semitone
In order to comply with the European General Data Protection Regulation (GDPR), we hereby draw your attention to NewPipe\'s privacy policy. Please read it carefully.
-\nYou must accept it to send us the bug report.
+ \nYou must accept it to send us the bug report.
Accept
Decline
@@ -687,7 +689,7 @@
Remove duplicates?
Do you want to remove all duplicate streams in this playlist?
Streams that have been watched before and after being added to the playlist will be removed.
-\nAre you sure\?
+ \nAre you sure\?
Remove partially watched streams
Due to ExoPlayer constraints the seek duration was set to %d seconds
@@ -739,16 +741,16 @@
Enable fast mode
Disable fast mode
Do you think feed loading is too slow\? If so, try enabling fast loading (you can change it in settings or by pressing the button below).
-\n
-\nNewPipe offers two feed loading strategies:
-\n• Fetching the whole subscription channel, which is slow but complete.
-\n• Using a dedicated service endpoint, which is fast but usually not complete.
-\n
-\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.
-\n
-\nYouTube is an example of a service that offers this fast method with its RSS feed.
-\n
-\nSo the choice boils down to what you prefer: speed or precise information.
+ \n
+ \nNewPipe offers two feed loading strategies:
+ \n• Fetching the whole subscription channel, which is slow but complete.
+ \n• Using a dedicated service endpoint, which is fast but usually not complete.
+ \n
+ \nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.
+ \n
+ \nYouTube is an example of a service that offers this fast method with its RSS feed.
+ \n
+ \nSo the choice boils down to what you prefer: speed or precise information.
Show the following streams
Show/Hide streams
Fetch channel tabs
@@ -764,9 +766,9 @@
Chapters
No app on your device can open this
No appropriate file manager was found for this action.
-\nPlease install a file manager or try to disable \'%s\' in the download settings
+ \nPlease install a file manager or try to disable \'%s\' in the download settings
No appropriate file manager was found for this action.
-\nPlease install a Storage Access Framework compatible file manager
+ \nPlease install a Storage Access Framework compatible file manager
This content is not available in your country.
This is a SoundCloud Go+ track, at least in your country, so it cannot be streamed or downloaded by NewPipe.
This content is private, so it cannot be streamed or downloaded by NewPipe.
@@ -903,4 +905,4 @@
NewPipe is dropping support for Android 5
Unfortunately NewPipe depends on a few libraries that dropped support for Android 5.0 and 5.1. The next NewPipe release will therefore only work on devices with Android 6 or higher, sadly. Read more in the blogpost.
Blogpost
-
+
\ No newline at end of file