mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-09-22 21:40:52 +02:00
Compare commits
132 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
50f3131f1a | ||
![]() |
fcaebc838e | ||
![]() |
cde32a8aed | ||
![]() |
ec3efea05a | ||
![]() |
5ac71e0579 | ||
![]() |
d04ecbcb0a | ||
![]() |
e4987d9a59 | ||
![]() |
155c6e94a3 | ||
![]() |
4e285a4e70 | ||
![]() |
9c00e681bb | ||
![]() |
160891592b | ||
![]() |
085d1e0d38 | ||
![]() |
4ee1cd5826 | ||
![]() |
dc7fce86a5 | ||
![]() |
10c9661369 | ||
![]() |
3901ffca17 | ||
![]() |
cbd3308da6 | ||
![]() |
0ad6b3b88e | ||
![]() |
4e87f5aabc | ||
![]() |
2019af831a | ||
![]() |
1e076ea63d | ||
![]() |
4863084fa2 | ||
![]() |
7ba79171c7 | ||
![]() |
e3c2aea3cc | ||
![]() |
21c9530e8b | ||
![]() |
036196a487 | ||
![]() |
73855cacb7 | ||
![]() |
8dad6d7e1c | ||
![]() |
e5ffa2aa09 | ||
![]() |
8445c381c5 | ||
![]() |
fa46b7bf85 | ||
![]() |
7ce2250d85 | ||
![]() |
ef20d9b91a | ||
![]() |
fbee310261 | ||
![]() |
7d6bf4b0ca | ||
![]() |
210834fbe9 | ||
![]() |
a59660f421 | ||
![]() |
be5af0b777 | ||
![]() |
75e5fe7d27 | ||
![]() |
2985258074 | ||
![]() |
911ac65d1e | ||
![]() |
d2967f514b | ||
![]() |
a68c6a2cfc | ||
![]() |
733f6aae85 | ||
![]() |
1daece3bee | ||
![]() |
adddd48c1d | ||
![]() |
8c870cd3ca | ||
![]() |
bd5eda92a7 | ||
![]() |
cf09cef6d8 | ||
![]() |
b3f9f8275d | ||
![]() |
9597d474d0 | ||
![]() |
e6f2e9791c | ||
![]() |
31b1370270 | ||
![]() |
064a4ce798 | ||
![]() |
ac5843edb0 | ||
![]() |
a1f64e4774 | ||
![]() |
21d2ae709f | ||
![]() |
c5e509f069 | ||
![]() |
761c0ff9ac | ||
![]() |
ce8289e753 | ||
![]() |
2dd4f8b04a | ||
![]() |
b4615f7655 | ||
![]() |
fcaa787060 | ||
![]() |
23c1fc3544 | ||
![]() |
a4037a8268 | ||
![]() |
61ee1c61df | ||
![]() |
69f95f4148 | ||
![]() |
212a413e93 | ||
![]() |
de4b5a8f0f | ||
![]() |
1228ce277f | ||
![]() |
bd6fdd625a | ||
![]() |
7de17ad949 | ||
![]() |
7ab11a8379 | ||
![]() |
70e0085596 | ||
![]() |
f9ccc19df5 | ||
![]() |
5c69568c7f | ||
![]() |
1d69bd48be | ||
![]() |
5b435c586e | ||
![]() |
71e46d1eca | ||
![]() |
238aff7c31 | ||
![]() |
a1435bd566 | ||
![]() |
59d8c570b7 | ||
![]() |
8f34f69397 | ||
![]() |
47af21d248 | ||
![]() |
c2a3c1cb8f | ||
![]() |
1e2d76a686 | ||
![]() |
34468c16ad | ||
![]() |
b84c6b4b32 | ||
![]() |
8395cf8d5a | ||
![]() |
c2bf7f09ce | ||
![]() |
c2762d3b5e | ||
![]() |
01d996a5c0 | ||
![]() |
50739277c4 | ||
![]() |
0fef4e6e2e | ||
![]() |
218012558a | ||
![]() |
e40e86500b | ||
![]() |
6f0942ac6e | ||
![]() |
a67927c29c | ||
![]() |
7e50eed95e | ||
![]() |
173b6c3f00 | ||
![]() |
7646c683b5 | ||
![]() |
047fe21c14 | ||
![]() |
b59a601b52 | ||
![]() |
bb495f567c | ||
![]() |
aa1db617d5 | ||
![]() |
ec5cfe0019 | ||
![]() |
fd5626e9e2 | ||
![]() |
53bf3420e7 | ||
![]() |
127a27315e | ||
![]() |
671441bdf8 | ||
![]() |
8ea98b64aa | ||
![]() |
4904b48f5c | ||
![]() |
a311519314 | ||
![]() |
1dc146322c | ||
![]() |
0f551baf37 | ||
![]() |
b9190eddfe | ||
![]() |
44dada9e60 | ||
![]() |
1b8c517e3e | ||
![]() |
20602889be | ||
![]() |
4b06536582 | ||
![]() |
621b38c98b | ||
![]() |
321cf8bf7d | ||
![]() |
762cdc812c | ||
![]() |
dae5aa38a8 | ||
![]() |
7d42e50f5b | ||
![]() |
a4c083e7f9 | ||
![]() |
e4f202834c | ||
![]() |
6e0c380409 | ||
![]() |
4cdf6eda2c | ||
![]() |
652d50173e | ||
![]() |
248ca5ee12 | ||
![]() |
2e771cd65a |
@@ -17,7 +17,7 @@
|
||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
|
||||
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
|
@@ -16,8 +16,8 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdk 19
|
||||
targetSdk 29
|
||||
versionCode 986
|
||||
versionName "0.23.0"
|
||||
versionCode 988
|
||||
versionName "0.23.2"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -107,7 +107,7 @@ ext {
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.17.1'
|
||||
googleAutoServiceVersion = '1.0.1'
|
||||
groupieVersion = '2.10.0'
|
||||
groupieVersion = '2.10.1'
|
||||
markwonVersion = '4.6.2'
|
||||
|
||||
leakCanaryVersion = '2.5'
|
||||
@@ -190,7 +190,7 @@ dependencies {
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:ac1c22d81c65b7b0c5427f4e1989f5256d617f32'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:76aad92fa54524f20c3338ab568c9cd6b50c9d33'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
@@ -221,10 +221,9 @@ dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation "androidx.work:work-runtime:${androidxWorkVersion}"
|
||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
@@ -262,7 +261,7 @@ dependencies {
|
||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.8.4"
|
||||
implementation "ch.acra:acra-core:5.9.3"
|
||||
|
||||
// Properly restarting
|
||||
implementation 'com.jakewharton:process-phoenix:2.1.2'
|
||||
|
@@ -91,7 +91,12 @@ class StreamItemAdapterTest {
|
||||
context,
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
(0 until 5).map {
|
||||
SubtitlesStream(MediaFormat.SRT, "pt-BR", "https://example.com", false)
|
||||
SubtitlesStream.Builder()
|
||||
.setContent("https://example.com", true)
|
||||
.setMediaFormat(MediaFormat.SRT)
|
||||
.setLanguageCode("pt-BR")
|
||||
.setAutoGenerated(false)
|
||||
.build()
|
||||
},
|
||||
context
|
||||
),
|
||||
@@ -108,7 +113,14 @@ class StreamItemAdapterTest {
|
||||
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||
context,
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
(0 until 5).map { AudioStream("https://example.com/$it", MediaFormat.OPUS, 192) },
|
||||
(0 until 5).map {
|
||||
AudioStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
.setContent("https://example.com/$it", true)
|
||||
.setMediaFormat(MediaFormat.OPUS)
|
||||
.setAverageBitrate(192)
|
||||
.build()
|
||||
},
|
||||
context
|
||||
),
|
||||
null
|
||||
@@ -126,7 +138,13 @@ class StreamItemAdapterTest {
|
||||
private fun getVideoStreams(vararg videoOnly: Boolean) =
|
||||
StreamItemAdapter.StreamSizeWrapper(
|
||||
videoOnly.map {
|
||||
VideoStream("https://example.com", MediaFormat.MPEG_4, "720p", it)
|
||||
VideoStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
.setContent("https://example.com", true)
|
||||
.setMediaFormat(MediaFormat.MPEG_4)
|
||||
.setResolution("720p")
|
||||
.setIsVideoOnly(it)
|
||||
.build()
|
||||
},
|
||||
context
|
||||
)
|
||||
@@ -138,8 +156,16 @@ class StreamItemAdapterTest {
|
||||
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
|
||||
getSecondaryStreamsFromList(
|
||||
shouldBeValid.map {
|
||||
if (it) AudioStream("https://example.com", MediaFormat.OPUS, 192)
|
||||
else null
|
||||
if (it) {
|
||||
AudioStream.Builder()
|
||||
.setId(Stream.ID_UNKNOWN)
|
||||
.setContent("https://example.com", true)
|
||||
.setMediaFormat(MediaFormat.OPUS)
|
||||
.setAverageBitrate(192)
|
||||
.build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@@ -205,7 +205,7 @@ public class App extends MultiDexApplication {
|
||||
return;
|
||||
}
|
||||
|
||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder(this)
|
||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
||||
.withBuildConfigClass(BuildConfig.class);
|
||||
ACRA.init(this, acraConfig);
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@ import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public final class DownloaderImpl extends Downloader {
|
||||
public static final String USER_AGENT
|
||||
= "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0";
|
||||
= "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
||||
= "youtube_restricted_mode_key";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
@@ -42,18 +41,19 @@ public class StreamHistoryEntity {
|
||||
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||
private long repeatCount;
|
||||
|
||||
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate,
|
||||
/**
|
||||
* @param streamUid the stream id this history item will refer to
|
||||
* @param accessDate the last time the stream was accessed
|
||||
* @param repeatCount the total number of views this stream received
|
||||
*/
|
||||
public StreamHistoryEntity(final long streamUid,
|
||||
@NonNull final OffsetDateTime accessDate,
|
||||
final long repeatCount) {
|
||||
this.streamUid = streamUid;
|
||||
this.accessDate = accessDate;
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate) {
|
||||
this(streamUid, accessDate, 1);
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
return streamUid;
|
||||
}
|
||||
|
@@ -12,8 +12,7 @@ import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
@@ -91,8 +90,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
newerStream.uid = existentMinimalStream.uid
|
||||
|
||||
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
|
||||
if (!isNewerStreamLive) {
|
||||
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
||||
|
||||
// Use the existent upload date if the newer stream does not have a better precision
|
||||
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,13 @@ import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.Info
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||
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.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@@ -65,7 +65,7 @@ class ErrorInfo(
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String) :
|
||||
this(throwable, userAction, SERVICE_NONE, request)
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) :
|
||||
this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
|
||||
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
|
||||
constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) :
|
||||
this(throwable, userAction, getInfoServiceName(info), request)
|
||||
|
||||
@@ -73,7 +73,7 @@ class ErrorInfo(
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String) :
|
||||
this(throwable, userAction, SERVICE_NONE, request)
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, serviceId: Int) :
|
||||
this(throwable, userAction, NewPipe.getNameOfService(serviceId), request)
|
||||
this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request)
|
||||
constructor(throwable: List<Throwable>, userAction: UserAction, request: String, info: Info?) :
|
||||
this(throwable, userAction, getInfoServiceName(info), request)
|
||||
|
||||
@@ -95,7 +95,7 @@ class ErrorInfo(
|
||||
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
|
||||
|
||||
private fun getInfoServiceName(info: Info?) =
|
||||
if (info == null) SERVICE_NONE else NewPipe.getNameOfService(info.serviceId)
|
||||
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
|
||||
|
||||
@StringRes
|
||||
private fun getMessageStringId(
|
||||
|
@@ -15,7 +15,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
@@ -106,7 +105,7 @@ class ErrorPanelHelper(
|
||||
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
|
||||
errorServiceInfoTextView.text = context.resources.getString(
|
||||
R.string.service_provides_reason,
|
||||
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
|
||||
ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "<unknown>"
|
||||
)
|
||||
errorServiceInfoTextView.isVisible = true
|
||||
|
||||
|
@@ -31,6 +31,7 @@ import android.view.WindowManager;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -94,6 +95,7 @@ import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
@@ -121,6 +123,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientat
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||
|
||||
public final class VideoDetailFragment
|
||||
extends BaseStateFragment<StreamInfo>
|
||||
@@ -186,8 +189,6 @@ public final class VideoDetailFragment
|
||||
@Nullable
|
||||
private Disposable positionSubscriber = null;
|
||||
|
||||
private List<VideoStream> sortedVideoStreams;
|
||||
private int selectedVideoStreamIndex = -1;
|
||||
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
@@ -663,8 +664,7 @@ public final class VideoDetailFragment
|
||||
binding.detailControlsCrashThePlayer.setOnClickListener(
|
||||
v -> VideoDetailPlayerCrasher.onCrashThePlayer(
|
||||
this.getContext(),
|
||||
this.player,
|
||||
getLayoutInflater())
|
||||
this.player)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1093,9 +1093,6 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void openBackgroundPlayer(final boolean append) {
|
||||
final AudioStream audioStream = currentInfo.getAudioStreams()
|
||||
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
|
||||
|
||||
final boolean useExternalAudioPlayer = PreferenceManager
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||
@@ -1110,7 +1107,17 @@ public final class VideoDetailFragment
|
||||
if (!useExternalAudioPlayer) {
|
||||
openNormalBackgroundPlayer(append);
|
||||
} else {
|
||||
startOnExternalPlayer(activity, currentInfo, audioStream);
|
||||
final List<AudioStream> audioStreams = getUrlAndNonTorrentStreams(
|
||||
currentInfo.getAudioStreams());
|
||||
final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams);
|
||||
|
||||
if (index == -1) {
|
||||
Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
startOnExternalPlayer(activity, currentInfo, audioStreams.get(index));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1613,14 +1620,6 @@ public final class VideoDetailFragment
|
||||
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
|
||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||
|
||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(
|
||||
activity,
|
||||
info.getVideoStreams(),
|
||||
info.getVideoOnlyStreams(),
|
||||
false,
|
||||
false);
|
||||
selectedVideoStreamIndex = ListHelper
|
||||
.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||
updateProgressInfo(info);
|
||||
initThumbnailViews(info);
|
||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||
@@ -1646,8 +1645,8 @@ public final class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM
|
||||
|| info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE);
|
||||
binding.detailControlsDownload.setVisibility(
|
||||
StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE);
|
||||
binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty()
|
||||
? View.GONE : View.VISIBLE);
|
||||
|
||||
@@ -1688,12 +1687,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
try {
|
||||
final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
|
||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
||||
|
||||
final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo);
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG,
|
||||
@@ -1723,8 +1717,7 @@ public final class VideoDetailFragment
|
||||
binding.detailPositionView.setVisibility(View.GONE);
|
||||
// TODO: Remove this check when separation of concerns is done.
|
||||
// (live streams weren't getting updated because they are mixed)
|
||||
if (!info.getStreamType().equals(StreamType.LIVE_STREAM)
|
||||
&& !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
|
||||
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -2152,25 +2145,52 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
private void showExternalPlaybackDialog() {
|
||||
if (sortedVideoStreams == null) {
|
||||
if (currentInfo == null) {
|
||||
return;
|
||||
}
|
||||
final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()];
|
||||
for (int i = 0; i < sortedVideoStreams.size(); i++) {
|
||||
resolutions[i] = sortedVideoStreams.get(i).getResolution();
|
||||
}
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||
ShareUtils.openUrlInBrowser(requireActivity(), url)
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.select_quality_external_players);
|
||||
builder.setNeutralButton(R.string.open_in_browser, (dialog, i) ->
|
||||
ShareUtils.openUrlInBrowser(requireActivity(), url));
|
||||
|
||||
final List<VideoStream> videoStreamsForExternalPlayers =
|
||||
ListHelper.getSortedStreamVideosList(
|
||||
activity,
|
||||
getUrlAndNonTorrentStreams(currentInfo.getVideoStreams()),
|
||||
getUrlAndNonTorrentStreams(currentInfo.getVideoOnlyStreams()),
|
||||
false,
|
||||
false
|
||||
);
|
||||
// Maybe there are no video streams available, show just `open in browser` button
|
||||
if (resolutions.length > 0) {
|
||||
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> {
|
||||
dialog.dismiss();
|
||||
startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i));
|
||||
|
||||
if (videoStreamsForExternalPlayers.isEmpty()) {
|
||||
builder.setMessage(R.string.no_video_streams_available_for_external_players);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
|
||||
} else {
|
||||
final int selectedVideoStreamIndexForExternalPlayers =
|
||||
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
|
||||
final CharSequence[] resolutions =
|
||||
new CharSequence[videoStreamsForExternalPlayers.size()];
|
||||
|
||||
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
|
||||
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
|
||||
}
|
||||
);
|
||||
|
||||
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
|
||||
null);
|
||||
builder.setNegativeButton(R.string.cancel, null);
|
||||
builder.setPositiveButton(R.string.ok, (dialog, i) -> {
|
||||
final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
// We don't have to manage the index validity because if there is no stream
|
||||
// available for external players, this code will be not executed and if there is
|
||||
// no stream which matches the default resolution, 0 is returned by
|
||||
// ListHelper.getDefaultResolutionIndex.
|
||||
// The index cannot be outside the bounds of the list as its always between 0 and
|
||||
// the list size - 1, .
|
||||
startOnExternalPlayer(activity, currentInfo,
|
||||
videoStreamsForExternalPlayers.get(index));
|
||||
});
|
||||
}
|
||||
builder.show();
|
||||
}
|
||||
|
@@ -1,5 +1,9 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
|
||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED;
|
||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
@@ -29,10 +33,6 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW;
|
||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED;
|
||||
import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED;
|
||||
|
||||
/**
|
||||
* Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
|
||||
*/
|
||||
@@ -97,8 +97,7 @@ public final class VideoDetailPlayerCrasher {
|
||||
|
||||
public static void onCrashThePlayer(
|
||||
@NonNull final Context context,
|
||||
@Nullable final Player player,
|
||||
@NonNull final LayoutInflater layoutInflater
|
||||
@Nullable final Player player
|
||||
) {
|
||||
if (player == null) {
|
||||
Log.d(TAG, "Player is not available");
|
||||
@@ -109,16 +108,15 @@ public final class VideoDetailPlayerCrasher {
|
||||
}
|
||||
|
||||
// -- Build the dialog/UI --
|
||||
|
||||
final Context themeWrapperContext = getThemeWrapperContext(context);
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
|
||||
.list;
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
|
||||
final SingleChoiceDialogViewBinding binding =
|
||||
SingleChoiceDialogViewBinding.inflate(inflater);
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext)
|
||||
.setTitle("Choose an exception")
|
||||
.setView(radioGroup)
|
||||
.setView(binding.getRoot())
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
@@ -136,11 +134,9 @@ public final class VideoDetailPlayerCrasher {
|
||||
);
|
||||
radioButton.setOnClickListener(v -> {
|
||||
tryCrashPlayerWith(player, entry.getValue().get());
|
||||
if (alertDialog != null) {
|
||||
alertDialog.cancel();
|
||||
}
|
||||
});
|
||||
radioGroup.addView(radioButton);
|
||||
binding.list.addView(radioButton);
|
||||
}
|
||||
|
||||
alertDialog.show();
|
||||
|
@@ -77,6 +77,8 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private Disposable subscribeButtonMonitor;
|
||||
|
||||
private boolean channelContentNotSupported = false;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -130,6 +132,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
channelBinding = FragmentChannelBinding.bind(rootView);
|
||||
showContentNotSupportedIfNeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -524,9 +527,12 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
playlistControlBinding.getRoot().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
channelContentNotSupported = false;
|
||||
for (final Throwable throwable : result.getErrors()) {
|
||||
if (throwable instanceof ContentNotSupportedException) {
|
||||
showContentNotSupported();
|
||||
channelContentNotSupported = true;
|
||||
showContentNotSupportedIfNeeded();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,7 +564,13 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
});
|
||||
}
|
||||
|
||||
private void showContentNotSupported() {
|
||||
private void showContentNotSupportedIfNeeded() {
|
||||
// channelBinding might not be initialized when handleResult() is called
|
||||
// (e.g. after rotating the screen, #6696)
|
||||
if (!channelContentNotSupported || channelBinding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
|
||||
channelBinding.channelKaomoji.setText("(︶︹︺)");
|
||||
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
|
@@ -4,7 +4,6 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@@ -18,7 +17,6 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.shape.CornerFamily;
|
||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||
@@ -28,6 +26,7 @@ import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||
import org.schabi.newpipe.databinding.PlaylistHeaderBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
@@ -41,6 +40,8 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
@@ -49,13 +50,13 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
@@ -237,6 +238,17 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
case R.id.menu_item_bookmark:
|
||||
onBookmarkClicked();
|
||||
break;
|
||||
case R.id.menu_item_append_playlist:
|
||||
disposables.add(PlaylistDialog.createCorrespondingDialog(
|
||||
getContext(),
|
||||
getPlayQueue()
|
||||
.getStreams()
|
||||
.stream()
|
||||
.map(StreamEntity::new)
|
||||
.collect(Collectors.toList()),
|
||||
dialog -> dialog.show(getFM(), TAG)
|
||||
));
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -293,10 +305,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
||||
.setAllCorners(CornerFamily.ROUNDED, 0f)
|
||||
.build(); // this turns the image back into a square
|
||||
headerBinding.uploaderAvatarView.setShapeAppearanceModel(model);
|
||||
headerBinding.uploaderAvatarView.setStrokeColor(
|
||||
ColorStateList.valueOf(ContextCompat.getColor(
|
||||
requireContext(), R.color.transparent_background_color))
|
||||
);
|
||||
headerBinding.uploaderAvatarView.setStrokeColor(AppCompatResources
|
||||
.getColorStateList(requireContext(), R.color.transparent_background_color));
|
||||
headerBinding.uploaderAvatarView.setImageDrawable(
|
||||
AppCompatResources.getDrawable(requireContext(),
|
||||
R.drawable.ic_radio)
|
||||
|
@@ -24,6 +24,7 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -269,8 +270,7 @@ public final class InfoItemDialog {
|
||||
*/
|
||||
public Builder addStartHereEntries() {
|
||||
addEntry(StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND);
|
||||
if (infoItem.getStreamType() != StreamType.AUDIO_STREAM
|
||||
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
|
||||
if (!StreamTypeUtil.isAudio(infoItem.getStreamType())) {
|
||||
addEntry(StreamDialogDefaultEntry.START_HERE_ON_POPUP);
|
||||
}
|
||||
return this;
|
||||
@@ -285,9 +285,7 @@ public final class InfoItemDialog {
|
||||
final boolean isWatchHistoryEnabled = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
||||
if (isWatchHistoryEnabled
|
||||
&& infoItem.getStreamType() != StreamType.LIVE_STREAM
|
||||
&& infoItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM) {
|
||||
if (isWatchHistoryEnabled && !StreamTypeUtil.isLiveStream(infoItem.getStreamType())) {
|
||||
addEntry(StreamDialogDefaultEntry.MARK_AS_WATCHED);
|
||||
}
|
||||
return this;
|
||||
|
@@ -11,12 +11,12 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -70,8 +70,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM
|
||||
|| item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) {
|
||||
} else if (StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||
itemDurationView.setText(R.string.duration_live);
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.live_duration_background_color));
|
||||
@@ -96,9 +95,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
case VIDEO_STREAM:
|
||||
case LIVE_STREAM:
|
||||
case AUDIO_LIVE_STREAM:
|
||||
case POST_LIVE_STREAM:
|
||||
case POST_LIVE_AUDIO_STREAM:
|
||||
enableLongClick(item);
|
||||
break;
|
||||
case FILE:
|
||||
case NONE:
|
||||
default:
|
||||
disableLongClick();
|
||||
@@ -114,7 +114,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
final StreamStateEntity state
|
||||
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
||||
if (state != null && item.getDuration() > 0
|
||||
&& item.getStreamType() != StreamType.LIVE_STREAM) {
|
||||
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
|
@@ -300,14 +300,7 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
|
||||
}
|
||||
}
|
||||
|
||||
fun View.slideUp(
|
||||
duration: Long,
|
||||
delay: Long,
|
||||
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float
|
||||
) {
|
||||
slideUp(duration, delay, translationPercent, null)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun View.slideUp(
|
||||
duration: Long,
|
||||
delay: Long = 0L,
|
||||
|
@@ -25,7 +25,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
@@ -37,7 +36,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
@@ -80,6 +78,7 @@ import org.schabi.newpipe.util.DeviceUtils
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
||||
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
|
||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.function.Consumer
|
||||
@@ -579,19 +578,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
lastNewItemsCount = highlightCount
|
||||
}
|
||||
|
||||
private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
|
||||
return androidx.core.content.ContextCompat.getDrawable(
|
||||
context,
|
||||
android.util.TypedValue().apply {
|
||||
context.theme.resolveAttribute(
|
||||
attrResId,
|
||||
this,
|
||||
true
|
||||
)
|
||||
}.resourceId
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNewItemsLoaded() {
|
||||
tryGetNewItemsLoadedButton()?.clearAnimation()
|
||||
tryGetNewItemsLoadedButton()
|
||||
|
@@ -14,6 +14,8 @@ import org.schabi.newpipe.databinding.ListStreamItemBinding
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_AUDIO_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
@@ -109,7 +111,7 @@ data class StreamItem(
|
||||
}
|
||||
|
||||
override fun isLongClickable() = when (stream.streamType) {
|
||||
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
|
||||
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM, POST_LIVE_STREAM, POST_LIVE_AUDIO_STREAM -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
@@ -128,13 +128,11 @@ public class HistoryRecordManager {
|
||||
|
||||
// Add a history entry
|
||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
if (latestEntry != null) {
|
||||
streamHistoryTable.delete(latestEntry);
|
||||
latestEntry.setAccessDate(currentTime);
|
||||
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
||||
return streamHistoryTable.insert(latestEntry);
|
||||
if (latestEntry == null) {
|
||||
// never actually viewed: add history entry but with 0 views
|
||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 0));
|
||||
} else {
|
||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime));
|
||||
return 0L;
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
@@ -155,7 +153,8 @@ public class HistoryRecordManager {
|
||||
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
||||
return streamHistoryTable.insert(latestEntry);
|
||||
} else {
|
||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime));
|
||||
// just viewed for the first time: set 1 view
|
||||
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 1));
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
@@ -11,12 +11,12 @@ import androidx.core.content.ContextCompat;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -59,7 +59,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
itemVideoTitleView.setText(item.getStreamEntity().getTitle());
|
||||
itemAdditionalDetailsView.setText(Localization
|
||||
.concatenateStrings(item.getStreamEntity().getUploader(),
|
||||
NewPipe.getNameOfService(item.getStreamEntity().getServiceId())));
|
||||
ServiceHelper.getNameOfServiceById(item.getStreamEntity().getServiceId())));
|
||||
|
||||
if (item.getStreamEntity().getDuration() > 0) {
|
||||
itemDurationView.setText(Localization
|
||||
|
@@ -11,12 +11,12 @@ import androidx.core.content.ContextCompat;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -70,11 +70,12 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
|
||||
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
|
||||
final DateTimeFormatter dateTimeFormatter) {
|
||||
final String watchCount = Localization
|
||||
.shortViewCount(itemBuilder.getContext(), entry.getWatchCount());
|
||||
final String uploadDate = dateTimeFormatter.format(entry.getLatestAccessDate());
|
||||
final String serviceName = NewPipe.getNameOfService(entry.getStreamEntity().getServiceId());
|
||||
return Localization.concatenateStrings(watchCount, uploadDate, serviceName);
|
||||
return Localization.concatenateStrings(
|
||||
// watchCount
|
||||
Localization.shortViewCount(itemBuilder.getContext(), entry.getWatchCount()),
|
||||
dateTimeFormatter.format(entry.getLatestAccessDate()),
|
||||
// serviceName
|
||||
ServiceHelper.getNameOfServiceById(entry.getStreamEntity().getServiceId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -5,11 +5,11 @@ import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@@ -39,9 +39,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
// Here is where the uploader name is set in the bookmarked playlists library
|
||||
if (!TextUtils.isEmpty(item.getUploader())) {
|
||||
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
|
||||
NewPipe.getNameOfService(item.getServiceId())));
|
||||
ServiceHelper.getNameOfServiceById(item.getServiceId())));
|
||||
} else {
|
||||
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
|
||||
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
|
||||
}
|
||||
|
||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
@@ -419,9 +419,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
final PlaylistStreamEntry playlistItem = playlistIter.next();
|
||||
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
||||
playlistItem.getStreamId());
|
||||
final StreamStateEntity streamStateEntity = streamStatesIter.next();
|
||||
final long duration = playlistItem.toStreamInfoItem().getDuration();
|
||||
|
||||
final boolean hasState = streamStatesIter.next() != null;
|
||||
if (indexInHistory < 0 || hasState) {
|
||||
if (indexInHistory < 0 || (streamStateEntity != null
|
||||
&& !streamStateEntity.isFinished(duration))) {
|
||||
notWatchedItems.add(playlistItem);
|
||||
} else if (!thumbnailVideoRemoved
|
||||
&& playlistManager.getPlaylistThumbnail(playlistId)
|
||||
|
@@ -1,24 +1,24 @@
|
||||
package org.schabi.newpipe.local.subscription
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.SubMenu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.xwray.groupie.Group
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
@@ -34,6 +34,7 @@ import org.schabi.newpipe.databinding.FeedItemCarouselBinding
|
||||
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
|
||||
import org.schabi.newpipe.error.ErrorInfo
|
||||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
@@ -45,13 +46,10 @@ import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
|
||||
import org.schabi.newpipe.local.subscription.item.FeedImportExportItem
|
||||
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
|
||||
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
|
||||
@@ -59,6 +57,7 @@ import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.OnClickGesture
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
|
||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
@@ -74,12 +73,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
private lateinit var subscriptionManager: SubscriptionManager
|
||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private var subscriptionBroadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
|
||||
private val feedGroupsSection = Section()
|
||||
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
|
||||
private lateinit var importExportItem: FeedImportExportItem
|
||||
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
|
||||
private val subscriptionsSection = Section()
|
||||
|
||||
@@ -91,12 +87,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
@State
|
||||
@JvmField
|
||||
var itemsListState: Parcelable? = null
|
||||
|
||||
@State
|
||||
@JvmField
|
||||
var feedGroupsListState: Parcelable? = null
|
||||
@State
|
||||
@JvmField
|
||||
var importExportItemExpandedState: Boolean? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
@@ -120,20 +114,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
return inflater.inflate(R.layout.fragment_subscription, container, false)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setupBroadcastReceiver()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
|
||||
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
|
||||
importExportItemExpandedState = importExportItem.isExpanded
|
||||
|
||||
if (subscriptionBroadcastReceiver != null && activity != null) {
|
||||
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -150,28 +134,61 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
|
||||
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
|
||||
activity.supportActionBar?.setTitle(R.string.tab_subscriptions)
|
||||
|
||||
buildImportExportMenu(menu)
|
||||
}
|
||||
|
||||
private fun setupBroadcastReceiver() {
|
||||
if (activity == null) return
|
||||
private fun buildImportExportMenu(menu: Menu) {
|
||||
// -- Import --
|
||||
val importSubMenu = menu.addSubMenu(R.string.import_from)
|
||||
|
||||
if (subscriptionBroadcastReceiver != null) {
|
||||
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
|
||||
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { onImportPreviousSelected() }
|
||||
.setIcon(R.drawable.ic_backup)
|
||||
|
||||
for (service in ServiceList.all()) {
|
||||
val subscriptionExtractor = service.subscriptionExtractor ?: continue
|
||||
|
||||
val supportedSources = subscriptionExtractor.supportedSources
|
||||
if (supportedSources.isEmpty()) continue
|
||||
|
||||
addMenuItemToSubmenu(importSubMenu, service.serviceInfo.name) {
|
||||
onImportFromServiceSelected(service.serviceId)
|
||||
}
|
||||
.setIcon(ServiceHelper.getIcon(service.serviceId))
|
||||
}
|
||||
|
||||
val filters = IntentFilter()
|
||||
filters.addAction(EXPORT_COMPLETE_ACTION)
|
||||
filters.addAction(IMPORT_COMPLETE_ACTION)
|
||||
subscriptionBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
_binding?.itemsList?.post {
|
||||
importExportItem.isExpanded = false
|
||||
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
|
||||
}
|
||||
}
|
||||
// -- Export --
|
||||
val exportSubMenu = menu.addSubMenu(R.string.export_to)
|
||||
|
||||
addMenuItemToSubmenu(exportSubMenu, R.string.file) { onExportSelected() }
|
||||
.setIcon(R.drawable.ic_save)
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver!!, filters)
|
||||
private fun addMenuItemToSubmenu(
|
||||
subMenu: SubMenu,
|
||||
@StringRes title: Int,
|
||||
onClick: Runnable
|
||||
): MenuItem {
|
||||
return setClickListenerToMenuItem(subMenu.add(title), onClick)
|
||||
}
|
||||
|
||||
private fun addMenuItemToSubmenu(
|
||||
subMenu: SubMenu,
|
||||
title: String,
|
||||
onClick: Runnable
|
||||
): MenuItem {
|
||||
return setClickListenerToMenuItem(subMenu.add(title), onClick)
|
||||
}
|
||||
|
||||
private fun setClickListenerToMenuItem(
|
||||
menuItem: MenuItem,
|
||||
onClick: Runnable
|
||||
): MenuItem {
|
||||
menuItem.setOnMenuItemClickListener { _ ->
|
||||
onClick.run()
|
||||
true
|
||||
}
|
||||
return menuItem
|
||||
}
|
||||
|
||||
private fun onImportFromServiceSelected(serviceId: Int) {
|
||||
@@ -263,13 +280,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
|
||||
subscriptionsSection.setHideWhenEmpty(true)
|
||||
|
||||
importExportItem = FeedImportExportItem(
|
||||
{ onImportPreviousSelected() },
|
||||
{ onImportFromServiceSelected(it) },
|
||||
{ onExportSelected() },
|
||||
importExportItemExpandedState ?: false
|
||||
groupAdapter.add(
|
||||
Section(
|
||||
HeaderWithMenuItem(
|
||||
getString(R.string.tab_subscriptions)
|
||||
),
|
||||
listOf(subscriptionsSection)
|
||||
)
|
||||
)
|
||||
groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection)))
|
||||
}
|
||||
|
||||
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
|
||||
@@ -371,13 +389,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||
subscriptionsSection.update(result.subscriptions)
|
||||
subscriptionsSection.setHideWhenEmpty(false)
|
||||
|
||||
if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) {
|
||||
binding.itemsList.post {
|
||||
importExportItem.isExpanded = true
|
||||
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsListState != null) {
|
||||
binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
|
||||
itemsListState = null
|
||||
|
@@ -1,5 +1,11 @@
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
@@ -40,12 +46,6 @@ import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||
|
||||
public class SubscriptionsImportFragment extends BaseFragment {
|
||||
@State
|
||||
int currentServiceId = Constants.NO_SERVICE_ID;
|
||||
@@ -89,7 +89,7 @@ public class SubscriptionsImportFragment extends BaseFragment {
|
||||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||
ErrorUtil.showSnackbar(activity,
|
||||
new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT,
|
||||
NewPipe.getNameOfService(currentServiceId),
|
||||
ServiceHelper.getNameOfServiceById(currentServiceId),
|
||||
"Service does not support importing subscriptions",
|
||||
R.string.general_error));
|
||||
activity.finish();
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package org.schabi.newpipe.local.subscription.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
@@ -9,7 +8,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isGone
|
||||
@@ -127,7 +126,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
// KitKat doesn't apply container's theme to <include> content
|
||||
val contrastColor = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.contrastColor))
|
||||
val contrastColor = AppCompatResources.getColorStateList(requireContext(), R.color.contrastColor)
|
||||
searchLayoutBinding.toolbarSearchEditText.setTextColor(contrastColor)
|
||||
searchLayoutBinding.toolbarSearchEditText.setHintTextColor(contrastColor.withAlpha(128))
|
||||
ImageViewCompat.setImageTintList(searchLayoutBinding.toolbarSearchClearIcon, contrastColor)
|
||||
|
@@ -1,122 +0,0 @@
|
||||
package org.schabi.newpipe.local.subscription.item
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.databinding.FeedImportExportGroupBinding
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||
import org.schabi.newpipe.ktx.animateRotation
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.views.CollapsibleView
|
||||
|
||||
class FeedImportExportItem(
|
||||
val onImportPreviousSelected: () -> Unit,
|
||||
val onImportFromServiceSelected: (Int) -> Unit,
|
||||
val onExportSelected: () -> Unit,
|
||||
var isExpanded: Boolean = false
|
||||
) : BindableItem<FeedImportExportGroupBinding>() {
|
||||
companion object {
|
||||
const val REFRESH_EXPANDED_STATUS = 123
|
||||
}
|
||||
|
||||
override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.contains(REFRESH_EXPANDED_STATUS)) {
|
||||
viewBinding.importExportOptions.apply { if (isExpanded) expand() else collapse() }
|
||||
return
|
||||
}
|
||||
|
||||
super.bind(viewBinding, position, payloads)
|
||||
}
|
||||
|
||||
override fun getLayout(): Int = R.layout.feed_import_export_group
|
||||
|
||||
override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int) {
|
||||
if (viewBinding.importFromOptions.childCount == 0) setupImportFromItems(viewBinding.importFromOptions)
|
||||
if (viewBinding.exportToOptions.childCount == 0) setupExportToItems(viewBinding.exportToOptions)
|
||||
|
||||
expandIconListener?.let { viewBinding.importExportOptions.removeListener(it) }
|
||||
expandIconListener = CollapsibleView.StateListener { newState ->
|
||||
viewBinding.importExportExpandIcon.animateRotation(
|
||||
250, if (newState == CollapsibleView.COLLAPSED) 0 else 180
|
||||
)
|
||||
}
|
||||
|
||||
viewBinding.importExportOptions.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
|
||||
viewBinding.importExportExpandIcon.rotation = if (isExpanded) 180F else 0F
|
||||
viewBinding.importExportOptions.ready()
|
||||
|
||||
viewBinding.importExportOptions.addListener(expandIconListener)
|
||||
viewBinding.importExport.setOnClickListener {
|
||||
viewBinding.importExportOptions.switchState()
|
||||
isExpanded = viewBinding.importExportOptions.currentState == CollapsibleView.EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbind(viewHolder: GroupieViewHolder<FeedImportExportGroupBinding>) {
|
||||
super.unbind(viewHolder)
|
||||
expandIconListener?.let { viewHolder.binding.importExportOptions.removeListener(it) }
|
||||
expandIconListener = null
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = FeedImportExportGroupBinding.bind(view)
|
||||
|
||||
private var expandIconListener: CollapsibleView.StateListener? = null
|
||||
|
||||
private fun addItemView(title: String, @DrawableRes icon: Int, container: ViewGroup): View {
|
||||
val itemRoot = View.inflate(container.context, R.layout.subscription_import_export_item, null)
|
||||
val titleView = itemRoot.findViewById<TextView>(android.R.id.text1)
|
||||
val iconView = itemRoot.findViewById<ImageView>(android.R.id.icon1)
|
||||
|
||||
titleView.text = title
|
||||
iconView.setImageResource(icon)
|
||||
|
||||
container.addView(itemRoot)
|
||||
return itemRoot
|
||||
}
|
||||
|
||||
private fun setupImportFromItems(listHolder: ViewGroup) {
|
||||
val previousBackupItem = addItemView(
|
||||
listHolder.context.getString(R.string.previous_export),
|
||||
R.drawable.ic_backup, listHolder
|
||||
)
|
||||
previousBackupItem.setOnClickListener { onImportPreviousSelected() }
|
||||
|
||||
val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE
|
||||
val services = listHolder.context.resources.getStringArray(R.array.service_list)
|
||||
for (serviceName in services) {
|
||||
try {
|
||||
val service = NewPipe.getService(serviceName)
|
||||
|
||||
val subscriptionExtractor = service.subscriptionExtractor ?: continue
|
||||
|
||||
val supportedSources = subscriptionExtractor.supportedSources
|
||||
if (supportedSources.isEmpty()) continue
|
||||
|
||||
val itemView = addItemView(serviceName, ServiceHelper.getIcon(service.serviceId), listHolder)
|
||||
val iconView = itemView.findViewById<ImageView>(android.R.id.icon1)
|
||||
iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN)
|
||||
|
||||
itemView.setOnClickListener { onImportFromServiceSelected(service.serviceId) }
|
||||
} catch (e: ExtractionException) {
|
||||
throw RuntimeException("Services array contains an entry that it's not a valid service name ($serviceName)", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupExportToItems(listHolder: ViewGroup) {
|
||||
val previousBackupItem = addItemView(
|
||||
listHolder.context.getString(R.string.file),
|
||||
R.drawable.ic_save, listHolder
|
||||
)
|
||||
previousBackupItem.setOnClickListener { onExportSelected() }
|
||||
}
|
||||
}
|
@@ -97,7 +97,10 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||
getMenuInflater().inflate(R.menu.menu_play_queue, m);
|
||||
getMenuInflater().inflate(R.menu.menu_play_queue_bg, m);
|
||||
onMaybeMuteChanged();
|
||||
// to avoid null reference
|
||||
if (player != null) {
|
||||
onPlaybackParameterChanged(player.getPlaybackParameters());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -88,6 +88,7 @@ import android.provider.Settings;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -118,7 +119,6 @@ import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.collection.ArraySet;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.GestureDetectorCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -150,7 +150,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Target;
|
||||
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
@@ -387,7 +386,7 @@ public final class Player implements
|
||||
private static final float MAX_GESTURE_LENGTH = 0.75f;
|
||||
|
||||
private int maxGestureLength; // scaled
|
||||
private GestureDetectorCompat gestureDetector;
|
||||
private GestureDetector gestureDetector;
|
||||
private PlayerGestureListener playerGestureListener;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -429,7 +428,7 @@ public final class Player implements
|
||||
setupBroadcastReceiver();
|
||||
|
||||
trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector());
|
||||
final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT,
|
||||
final PlayerDataSource dataSource = new PlayerDataSource(context,
|
||||
new DefaultBandwidthMeter.Builder(context).build());
|
||||
loadController = new LoadController();
|
||||
renderFactory = new DefaultRenderersFactory(context);
|
||||
@@ -555,7 +554,7 @@ public final class Player implements
|
||||
binding.playbackLiveSync.setOnClickListener(this);
|
||||
|
||||
playerGestureListener = new PlayerGestureListener(this, service);
|
||||
gestureDetector = new GestureDetectorCompat(context, playerGestureListener);
|
||||
gestureDetector = new GestureDetector(context, playerGestureListener);
|
||||
binding.getRoot().setOnTouchListener(playerGestureListener);
|
||||
|
||||
binding.queueButton.setOnClickListener(v -> onQueueClicked());
|
||||
@@ -1744,24 +1743,9 @@ public final class Player implements
|
||||
if (exoPlayerIsNull()) {
|
||||
return;
|
||||
}
|
||||
// Use duration of currentItem for non-live streams,
|
||||
// because HLS streams are fragmented
|
||||
// and thus the whole duration is not available to the player
|
||||
// TODO: revert #6307 when introducing proper HLS support
|
||||
final int duration;
|
||||
if (currentItem != null
|
||||
&& !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
|
||||
) {
|
||||
// convert seconds to milliseconds
|
||||
duration = (int) (currentItem.getDuration() * 1000);
|
||||
} else {
|
||||
duration = (int) simpleExoPlayer.getDuration();
|
||||
}
|
||||
onUpdateProgress(
|
||||
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||
duration,
|
||||
simpleExoPlayer.getBufferedPercentage()
|
||||
);
|
||||
|
||||
onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||
(int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
|
||||
}
|
||||
|
||||
private Disposable getProgressUpdateDisposable() {
|
||||
@@ -2501,22 +2485,31 @@ public final class Player implements
|
||||
Listener.super.onEvents(player, events);
|
||||
MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> {
|
||||
if (tag == currentMetadata) {
|
||||
return;
|
||||
return; // we still have the same metadata, no need to do anything
|
||||
}
|
||||
final StreamInfo previousInfo = Optional.ofNullable(currentMetadata)
|
||||
.flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null);
|
||||
currentMetadata = tag;
|
||||
if (!tag.getErrors().isEmpty()) {
|
||||
|
||||
if (!currentMetadata.getErrors().isEmpty()) {
|
||||
// new errors might have been added even if previousInfo == tag.getMaybeStreamInfo()
|
||||
final ErrorInfo errorInfo = new ErrorInfo(
|
||||
tag.getErrors().get(0),
|
||||
currentMetadata.getErrors(),
|
||||
UserAction.PLAY_STREAM,
|
||||
"Loading failed for [" + tag.getTitle() + "]: " + tag.getStreamUrl(),
|
||||
tag.getServiceId());
|
||||
"Loading failed for [" + currentMetadata.getTitle()
|
||||
+ "]: " + currentMetadata.getStreamUrl(),
|
||||
currentMetadata.getServiceId());
|
||||
ErrorUtil.createNotification(context, errorInfo);
|
||||
}
|
||||
tag.getMaybeStreamInfo().ifPresent(info -> {
|
||||
|
||||
currentMetadata.getMaybeStreamInfo().ifPresent(info -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ExoPlayer - onEvents() update stream info: " + info.getName());
|
||||
}
|
||||
if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) {
|
||||
// only update with the new stream info if it has actually changed
|
||||
updateMetadataWith(info);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3399,6 +3392,7 @@ public final class Player implements
|
||||
|
||||
switch (info.getStreamType()) {
|
||||
case AUDIO_STREAM:
|
||||
case POST_LIVE_AUDIO_STREAM:
|
||||
binding.surfaceView.setVisibility(View.GONE);
|
||||
binding.endScreen.setVisibility(View.VISIBLE);
|
||||
binding.playbackEndTime.setVisibility(View.VISIBLE);
|
||||
@@ -3417,6 +3411,7 @@ public final class Player implements
|
||||
break;
|
||||
|
||||
case VIDEO_STREAM:
|
||||
case POST_LIVE_STREAM:
|
||||
if (currentMetadata == null
|
||||
|| !currentMetadata.getMaybeQuality().isPresent()
|
||||
|| (info.getVideoStreams().isEmpty()
|
||||
@@ -3484,10 +3479,10 @@ public final class Player implements
|
||||
for (int i = 0; i < availableStreams.size(); i++) {
|
||||
final VideoStream videoStream = availableStreams.get(i);
|
||||
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
|
||||
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
|
||||
.getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
|
||||
}
|
||||
if (getSelectedVideoStream() != null) {
|
||||
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
|
||||
}
|
||||
qualityPopupMenu.setOnMenuItemClickListener(this);
|
||||
qualityPopupMenu.setOnDismissListener(this);
|
||||
@@ -3605,7 +3600,7 @@ public final class Player implements
|
||||
}
|
||||
|
||||
saveStreamProgressState(); //TODO added, check if good
|
||||
final String newResolution = availableStreams.get(menuItemIndex).resolution;
|
||||
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
|
||||
setRecovery();
|
||||
setPlaybackQuality(newResolution);
|
||||
reloadPlayQueueManager();
|
||||
@@ -3633,7 +3628,7 @@ public final class Player implements
|
||||
}
|
||||
isSomePopupMenuVisible = false; //TODO check if this works
|
||||
if (getSelectedVideoStream() != null) {
|
||||
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
|
||||
}
|
||||
if (isPlaying()) {
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||
@@ -4248,9 +4243,7 @@ public final class Player implements
|
||||
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
|
||||
reloadPlayQueueManager();
|
||||
} else {
|
||||
final StreamType streamType = info.getStreamType();
|
||||
if (streamType == StreamType.AUDIO_STREAM
|
||||
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
|
||||
if (StreamTypeUtil.isAudio(info.getStreamType())) {
|
||||
// Nothing to do more than setting the recovery position
|
||||
setRecovery();
|
||||
return;
|
||||
@@ -4285,13 +4278,15 @@ public final class Player implements
|
||||
* the content is not an audio content, but also if none of the following cases is met:
|
||||
*
|
||||
* <ul>
|
||||
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
|
||||
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
|
||||
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
|
||||
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a
|
||||
* {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};</li>
|
||||
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
|
||||
* {@link SourceType#LIVE_STREAM live source};</li>
|
||||
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
|
||||
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
|
||||
* {@link StreamType#LIVE_STREAM live stream} or a
|
||||
* {@link StreamType#VIDEO_STREAM video stream}, an
|
||||
* {@link StreamType#POST_LIVE_STREAM ended live stream}, or a
|
||||
* {@link StreamType#LIVE_STREAM live stream}.
|
||||
* </li>
|
||||
* </ul>
|
||||
@@ -4307,17 +4302,16 @@ public final class Player implements
|
||||
@NonNull final StreamInfo streamInfo,
|
||||
final int videoRendererIndex) {
|
||||
final StreamType streamType = streamInfo.getStreamType();
|
||||
final boolean isStreamTypeAudio = StreamTypeUtil.isAudio(streamType);
|
||||
|
||||
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
|
||||
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
|
||||
if (videoRendererIndex == RENDERER_UNAVAILABLE && !isStreamTypeAudio) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The content is an audio stream, an audio live stream, or a live stream with a live
|
||||
// source: it's not needed to reload the play queue manager because the stream source will
|
||||
// be the same
|
||||
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
|
||||
|| (streamType == StreamType.LIVE_STREAM
|
||||
if (isStreamTypeAudio || (streamType == StreamType.LIVE_STREAM
|
||||
&& sourceType == SourceType.LIVE_STREAM)) {
|
||||
return false;
|
||||
}
|
||||
@@ -4331,8 +4325,8 @@ public final class Player implements
|
||||
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
||||
// It's not needed to reload the play queue manager only if the content's stream type
|
||||
// is a video stream or a live stream
|
||||
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
|
||||
// is a video stream, a live stream or an ended live stream
|
||||
return !StreamTypeUtil.isVideo(streamType);
|
||||
}
|
||||
|
||||
// Other cases: the play queue manager reload is needed
|
||||
@@ -4428,7 +4422,7 @@ public final class Player implements
|
||||
return audioReactor;
|
||||
}
|
||||
|
||||
public GestureDetectorCompat getGestureDetector() {
|
||||
public GestureDetector getGestureDetector() {
|
||||
return gestureDetector;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,136 @@
|
||||
package org.schabi.newpipe.player.datasource;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* A {@link HlsDataSourceFactory} which allows playback of non-URI media HLS playlists for
|
||||
* {@link com.google.android.exoplayer2.source.hls.HlsMediaSource HlsMediaSource}s.
|
||||
*
|
||||
* <p>
|
||||
* If media requests are relative, the URI from which the manifest comes from (either the
|
||||
* manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the
|
||||
* content will be not playable, as it will be an invalid URL, or it may be treat as something
|
||||
* unexpected, for instance as a file for
|
||||
* {@link com.google.android.exoplayer2.upstream.DefaultDataSource DefaultDataSource}s.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* See {@link #createDataSource(int)} for changes and implementation details.
|
||||
* </p>
|
||||
*/
|
||||
public final class NonUriHlsDataSourceFactory implements HlsDataSourceFactory {
|
||||
|
||||
/**
|
||||
* Builder class of {@link NonUriHlsDataSourceFactory} instances.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private DataSource.Factory dataSourceFactory;
|
||||
private String playlistString;
|
||||
|
||||
/**
|
||||
* Set the {@link DataSource.Factory} which will be used to create non manifest contents
|
||||
* {@link DataSource}s.
|
||||
*
|
||||
* @param dataSourceFactoryForNonManifestContents the {@link DataSource.Factory} which will
|
||||
* be used to create non manifest contents
|
||||
* {@link DataSource}s, which cannot be null
|
||||
*/
|
||||
public void setDataSourceFactory(
|
||||
@NonNull final DataSource.Factory dataSourceFactoryForNonManifestContents) {
|
||||
this.dataSourceFactory = dataSourceFactoryForNonManifestContents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HLS playlist which will be used for manifests requests.
|
||||
*
|
||||
* @param hlsPlaylistString the string which correspond to the response of the HLS
|
||||
* manifest, which cannot be null or empty
|
||||
*/
|
||||
public void setPlaylistString(@NonNull final String hlsPlaylistString) {
|
||||
this.playlistString = hlsPlaylistString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link NonUriHlsDataSourceFactory} with the given data source factory and
|
||||
* the given HLS playlist.
|
||||
*
|
||||
* @return a {@link NonUriHlsDataSourceFactory}
|
||||
* @throws IllegalArgumentException if the data source factory is null or if the HLS
|
||||
* playlist string set is null or empty
|
||||
*/
|
||||
@NonNull
|
||||
public NonUriHlsDataSourceFactory build() {
|
||||
if (dataSourceFactory == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"No DataSource.Factory valid instance has been specified.");
|
||||
}
|
||||
|
||||
if (isNullOrEmpty(playlistString)) {
|
||||
throw new IllegalArgumentException("No HLS valid playlist has been specified.");
|
||||
}
|
||||
|
||||
return new NonUriHlsDataSourceFactory(dataSourceFactory,
|
||||
playlistString.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final byte[] playlistStringByteArray;
|
||||
|
||||
/**
|
||||
* Create a {@link NonUriHlsDataSourceFactory} instance.
|
||||
*
|
||||
* @param dataSourceFactory the {@link DataSource.Factory} which will be used to build
|
||||
* non manifests {@link DataSource}s, which must not be null
|
||||
* @param playlistStringByteArray a byte array of the HLS playlist, which must not be null
|
||||
*/
|
||||
private NonUriHlsDataSourceFactory(@NonNull final DataSource.Factory dataSourceFactory,
|
||||
@NonNull final byte[] playlistStringByteArray) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.playlistStringByteArray = playlistStringByteArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link DataSource} for the given data type.
|
||||
*
|
||||
* <p>
|
||||
* Contrary to {@link com.google.android.exoplayer2.source.hls.DefaultHlsDataSourceFactory
|
||||
* ExoPlayer's default implementation}, this implementation is not always using the
|
||||
* {@link DataSource.Factory} passed to the
|
||||
* {@link com.google.android.exoplayer2.source.hls.HlsMediaSource.Factory
|
||||
* HlsMediaSource.Factory} constructor, only when it's not
|
||||
* {@link C#DATA_TYPE_MANIFEST the manifest type}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This change allow 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).
|
||||
* </p>
|
||||
*
|
||||
* @param dataType the data type for which the {@link DataSource} will be used, which is one of
|
||||
* {@link C} {@code .DATA_TYPE_*} constants
|
||||
* @return a {@link DataSource} for the given data type
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource createDataSource(final int dataType) {
|
||||
// The manifest is already downloaded and provided with playlistStringByteArray, so we
|
||||
// don't need to download it again and we can use a ByteArrayDataSource instead
|
||||
if (dataType == C.DATA_TYPE_MANIFEST) {
|
||||
return new ByteArrayDataSource(playlistStringByteArray);
|
||||
}
|
||||
|
||||
return dataSourceFactory.createDataSource();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user