mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19ddba7e7c | |||
| 72efb21439 | |||
| aa8790ebdb | |||
| 6d491052ee | |||
| 87ff4691ce | |||
| 34d76e79ed | |||
| 31b43da96f | |||
| 0540e673e2 | |||
| 4e88a63809 | |||
| f7581f8a65 | |||
| e87a1c079c | |||
| 3f9477c246 | |||
| 05ed1e188e | |||
| f3d06e49f8 | |||
| f9a4b68967 | |||
| 3631cfe365 | |||
| 8766ae176e | |||
| 36b53d490f | |||
| f9b8b812a4 | |||
| ac9eae5272 |
@@ -525,6 +525,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var keepScreenOn: Boolean = true;
|
||||
|
||||
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var alwaysProxyRequests: Boolean = false;
|
||||
|
||||
/*TODO: Should we have a different casting quality?
|
||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.work.WorkManager
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.futo.platformplayer.activities.DeveloperActivity
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
@@ -491,6 +492,13 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FormField(R.string.test_playback, FieldForm.BUTTON,
|
||||
R.string.test_playback, 1)
|
||||
fun testPlayback(context: Context) {
|
||||
context.startActivity(MainActivity.getActionIntent(context, "TEST_PLAYBACK"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.parsers.HLS
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
@@ -710,7 +711,9 @@ class UISlideOverlays {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||
{ StatePlayer.instance.addToQueue(video); }),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_history, container.context.getString(R.string.add_to_history), "Mark as watched", "history",
|
||||
{ StateHistory.instance.markAsWatched(video); }),
|
||||
));
|
||||
|
||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -29,7 +30,6 @@ import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
@@ -42,7 +42,6 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||
import com.futo.platformplayer.listeners.OrientationManager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
import com.futo.platformplayer.states.*
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||
@@ -79,6 +78,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
private lateinit var _fragContainerVideoDetail: FragmentContainerView;
|
||||
private lateinit var _fragContainerOverlay: FrameLayout;
|
||||
|
||||
//Views
|
||||
private lateinit var _buttonIncognito: ImageView;
|
||||
|
||||
//Frags TopBar
|
||||
lateinit var _fragTopBarGeneral: GeneralTopBarFragment;
|
||||
lateinit var _fragTopBarSearch: SearchTopBarFragment;
|
||||
@@ -204,6 +206,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
setContentView(R.layout.activity_main);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
|
||||
runBlocking {
|
||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||
}
|
||||
@@ -290,6 +293,52 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
updateSegmentPaddings();
|
||||
};
|
||||
|
||||
|
||||
_buttonIncognito = findViewById(R.id.incognito_button);
|
||||
_buttonIncognito.elevation = -99f;
|
||||
_buttonIncognito.alpha = 0f;
|
||||
StateApp.instance.privateModeChanged.subscribe {
|
||||
//Messing with visibility causes some issues with layout ordering?
|
||||
if(it) {
|
||||
_buttonIncognito.elevation = 99f;
|
||||
_buttonIncognito.alpha = 1f;
|
||||
}
|
||||
else {
|
||||
_buttonIncognito.elevation = -99f;
|
||||
_buttonIncognito.alpha = 0f;
|
||||
}
|
||||
}
|
||||
_buttonIncognito.setOnClickListener {
|
||||
if(!StateApp.instance.privateMode)
|
||||
return@setOnClickListener;
|
||||
UIDialogs.showDialog(this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode",
|
||||
"Do you want to disable privacy mode? New videos will be tracked again.", null, 0,
|
||||
UIDialogs.Action("Cancel", {
|
||||
StateApp.instance.setPrivacyMode(true);
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Disable", {
|
||||
StateApp.instance.setPrivacyMode(false);
|
||||
}, UIDialogs.ActionStyle.DANGEROUS));
|
||||
};
|
||||
_fragVideoDetail.onFullscreenChanged.subscribe {
|
||||
Logger.i(TAG, "onFullscreenChanged ${it}");
|
||||
|
||||
if(it) {
|
||||
_buttonIncognito.elevation = -99f;
|
||||
_buttonIncognito.alpha = 0f;
|
||||
}
|
||||
else {
|
||||
if(StateApp.instance.privateMode) {
|
||||
_buttonIncognito.elevation = 99f;
|
||||
_buttonIncognito.alpha = 1f;
|
||||
}
|
||||
else {
|
||||
_buttonIncognito.elevation = -99f;
|
||||
_buttonIncognito.alpha = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatePlayer.instance.also {
|
||||
it.onQueueChanged.subscribe { shouldSwapCurrentItem ->
|
||||
if (!shouldSwapCurrentItem) {
|
||||
@@ -538,6 +587,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
"IMPORT_OPTIONS" -> {
|
||||
UIDialogs.showImportOptionsDialog(this);
|
||||
}
|
||||
"ACTION" -> {
|
||||
val action = intent.getStringExtra("ACTION");
|
||||
StateDeveloper.instance.testState = "TestPlayback";
|
||||
StateDeveloper.instance.testPlayback();
|
||||
}
|
||||
"TAB" -> {
|
||||
when(intent.getStringExtra("TAB")){
|
||||
"Sources" -> {
|
||||
@@ -1180,6 +1234,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
return sourcesIntent;
|
||||
}
|
||||
fun getActionIntent(context: Context, action: String) : Intent {
|
||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||
sourcesIntent.action = "ACTION";
|
||||
sourcesIntent.putExtra("ACTION", action);
|
||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
return sourcesIntent;
|
||||
}
|
||||
|
||||
fun getImportOptionsIntent(context: Context): Intent {
|
||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||
|
||||
@@ -13,13 +13,15 @@ class PlatformClientPool {
|
||||
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
||||
private var _poolCounter = 0;
|
||||
private val _poolName: String?;
|
||||
private val _privatePool: Boolean;
|
||||
|
||||
var isDead: Boolean = false
|
||||
private set;
|
||||
val onDead = Event2<JSClient, PlatformClientPool>();
|
||||
|
||||
constructor(parentClient: IPlatformClient, name: String? = null) {
|
||||
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
|
||||
_poolName = name;
|
||||
_privatePool = privatePool;
|
||||
if(parentClient !is JSClient)
|
||||
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
||||
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
||||
@@ -51,7 +53,7 @@ class PlatformClientPool {
|
||||
reserved = _pool.keys.find { !it.isBusy };
|
||||
if(reserved == null && _pool.size < capacity) {
|
||||
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
||||
reserved = _parent.getCopy();
|
||||
reserved = _parent.getCopy(_privatePool);
|
||||
|
||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||
StateApp.instance.handleCaptchaException(client, ex);
|
||||
|
||||
@@ -6,12 +6,14 @@ class PlatformMultiClientPool {
|
||||
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||
|
||||
private var _isFake = false;
|
||||
private var _privatePool = false;
|
||||
|
||||
constructor(name: String, maxCap: Int = -1) {
|
||||
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
|
||||
_name = name;
|
||||
_maxCap = if(maxCap > 0)
|
||||
maxCap
|
||||
else 99;
|
||||
_privatePool = isPrivatePool;
|
||||
}
|
||||
|
||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
||||
@@ -19,7 +21,7 @@ class PlatformMultiClientPool {
|
||||
return parentClient;
|
||||
val pool = synchronized(_clientPools) {
|
||||
if(!_clientPools.containsKey(parentClient))
|
||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
|
||||
this.onDead.subscribe { _, pool ->
|
||||
synchronized(_clientPools) {
|
||||
if(_clientPools[parentClient] == pool)
|
||||
|
||||
@@ -54,8 +54,8 @@ class DevJSClient : JSClient {
|
||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
||||
}
|
||||
|
||||
override fun getCopy(): JSClient {
|
||||
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
|
||||
override fun getCopy(privateCopy: Boolean): JSClient {
|
||||
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
|
||||
}
|
||||
|
||||
override fun initialize() {
|
||||
|
||||
@@ -164,13 +164,16 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
||||
this._context = context;
|
||||
this.config = descriptor.config;
|
||||
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
||||
this.descriptor = descriptor;
|
||||
_injectedSaveState = saveState;
|
||||
_auth = descriptor.getAuth();
|
||||
if(!withoutCredentials)
|
||||
_auth = descriptor.getAuth();
|
||||
else
|
||||
_auth = null;
|
||||
_captcha = descriptor.getCaptchaData();
|
||||
flags = descriptor.flags.toTypedArray();
|
||||
|
||||
@@ -190,8 +193,8 @@ open class JSClient : IPlatformClient {
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
|
||||
open fun getCopy(): JSClient {
|
||||
return JSClient(_context, descriptor, saveState(), _script);
|
||||
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
|
||||
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
|
||||
}
|
||||
|
||||
fun getUnderlyingPlugin(): V8Plugin {
|
||||
|
||||
+6
-2
@@ -5,6 +5,7 @@ import com.futo.platformplayer.SignatureProvider
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import kotlinx.serialization.Contextual
|
||||
import java.net.URL
|
||||
import java.util.UUID
|
||||
|
||||
@@ -77,7 +78,8 @@ class SourcePluginConfig(
|
||||
private var _allowUrlsLowerVal: List<String>? = null;
|
||||
private val _allowUrlsLower: List<String> get() {
|
||||
if(_allowUrlsLowerVal == null)
|
||||
_allowUrlsLowerVal = allowUrls.map { it.lowercase() };
|
||||
_allowUrlsLowerVal = allowUrls.map { it.lowercase() }
|
||||
.filter { it.length > 0 && (it[0] != '*' || (_allowRegex.matches(it))) };
|
||||
return _allowUrlsLowerVal!!;
|
||||
};
|
||||
|
||||
@@ -170,10 +172,12 @@ class SourcePluginConfig(
|
||||
return true;
|
||||
val uri = Uri.parse(url);
|
||||
val host = uri.host?.lowercase() ?: "";
|
||||
return _allowUrlsLower.any { it == host };
|
||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '*' && host.endsWith(it.substring(1))) };
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val _allowRegex = Regex("\\*\\.[a-z0-9]+\\.[a-z]+");
|
||||
|
||||
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
||||
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
||||
if(obj.sourceUrl == null)
|
||||
|
||||
+5
@@ -35,4 +35,9 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
|
||||
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
|
||||
audioChannels = _obj.getOrDefault(config, "audioChannels", contextName, 2) ?: 2;
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "RangeSource(url=[${getAudioUrl()}], itagId=[${itagId}], initStart=[${initStart}], initEnd=[${initEnd}], indexStart=[${indexStart}], indexEnd=[${indexEnd}]))";
|
||||
return super.toString()
|
||||
}
|
||||
}
|
||||
+5
@@ -33,4 +33,9 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
|
||||
indexStart = _obj.getOrDefault(config, "indexStart", contextName, null);
|
||||
indexEnd = _obj.getOrDefault(config, "indexEnd", contextName, null);
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "RangeSource(url=[${getVideoUrl()}], itagId=[${itagId}], initStart=[${initStart}], initEnd=[${initEnd}], indexStart=[${indexStart}], indexEnd=[${indexEnd}]))";
|
||||
return super.toString()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.os.Looper
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||
@@ -452,14 +453,22 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
if (videoSource is IVideoUrlSource) {
|
||||
val videoPath = "/video-${id}"
|
||||
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
|
||||
Logger.i(TAG, "Casting as singular video");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed);
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
} else if (audioSource is IAudioUrlSource) {
|
||||
val audioPath = "/audio-${id}"
|
||||
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
|
||||
Logger.i(TAG, "Casting as singular audio");
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed);
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
|
||||
} else if(videoSource is IHLSManifestSource) {
|
||||
if (ad is ChromecastCastingDevice) {
|
||||
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied HLS");
|
||||
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
|
||||
} else {
|
||||
@@ -467,7 +476,7 @@ class StateCasting {
|
||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
|
||||
}
|
||||
} else if(audioSource is IHLSManifestAudioSource) {
|
||||
if (ad is ChromecastCastingDevice) {
|
||||
if (proxyStreams || ad is ChromecastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as proxied audio HLS");
|
||||
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
|
||||
} else {
|
||||
@@ -667,8 +676,11 @@ class StateCasting {
|
||||
val audioUrl = url + audioPath;
|
||||
val subtitleUrl = url + subtitlePath;
|
||||
|
||||
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl);
|
||||
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl),
|
||||
HttpConstantHandler("GET", dashPath, dashContent,
|
||||
"application/dash+xml")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
@@ -699,13 +711,17 @@ class StateCasting {
|
||||
|
||||
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
val subtitlePath = "/subtitle-${id}";
|
||||
|
||||
val videoUrl = videoSource?.getVideoUrl();
|
||||
val audioUrl = audioSource?.getAudioUrl();
|
||||
val videoPath = "/video-${id}"
|
||||
val audioPath = "/audio-${id}"
|
||||
val subtitlePath = "/subtitle-${id}"
|
||||
|
||||
val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl();
|
||||
val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl();
|
||||
|
||||
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||
return@withContext subtitleSource.getSubtitlesURI();
|
||||
@@ -734,13 +750,28 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
|
||||
if (videoSource != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
}
|
||||
if (audioSource != null) {
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||
.withInjectedHost()
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
}
|
||||
|
||||
val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||
|
||||
Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl).");
|
||||
Logger.v(TAG) { "Dash manifest: $content" };
|
||||
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
||||
|
||||
return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "");
|
||||
}
|
||||
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); }
|
||||
|
||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||
@@ -1044,7 +1075,7 @@ class StateCasting {
|
||||
|
||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = ad !is FCastCastingDevice;
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
@@ -1090,8 +1121,11 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
|
||||
val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl);
|
||||
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||
|
||||
_castServer.addHandlerWithAllowAllOptions(
|
||||
HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl),
|
||||
HttpConstantHandler("GET", dashPath, dashContent,
|
||||
"application/dash+xml")
|
||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||
).withTag("cast");
|
||||
|
||||
@@ -52,6 +52,7 @@ class PackageBridge : V8Package {
|
||||
|
||||
@V8Function
|
||||
fun toast(str: String) {
|
||||
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.toast(str);
|
||||
|
||||
+20
-1
@@ -16,6 +16,7 @@ import androidx.core.animation.doOnEnd
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.dp
|
||||
@@ -222,6 +223,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
buttons.removeAt(faqIndex)
|
||||
buttons.add(if (buttons.size == 1) 1 else 0, button)
|
||||
}
|
||||
//Force privacy to be third
|
||||
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
|
||||
if (privacyIndex != -1) {
|
||||
val button = buttons[privacyIndex]
|
||||
buttons.removeAt(privacyIndex)
|
||||
buttons.add(if (buttons.size == 2) 2 else 1, button)
|
||||
}
|
||||
|
||||
for (data in buttons) {
|
||||
val button = MenuButton(context, data, _fragment, true);
|
||||
@@ -305,6 +313,16 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||
}))
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = false, { false }, {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
||||
"All requests will be processed anonymously (unauthenticated), playback and history tracking will be disabled.\n\nTap the icon to disable.", null, 0,
|
||||
UIDialogs.Action("Cancel", {
|
||||
StateApp.instance.setPrivacyMode(false);
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Enable", {
|
||||
StateApp.instance.setPrivacyMode(true);
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}))
|
||||
|
||||
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
||||
|
||||
@@ -370,7 +388,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
||||
}
|
||||
})
|
||||
//98 is reversed for buy button
|
||||
//96 is reserved for privacy button
|
||||
//98 is reserved for buy button
|
||||
//99 is reserved for more button
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -39,6 +39,7 @@ class VideoDetailFragment : MainFragment {
|
||||
private var _view : SingleViewTouchableMotionLayout? = null;
|
||||
|
||||
var isFullscreen : Boolean = false;
|
||||
val onFullscreenChanged = Event1<Boolean>();
|
||||
var isTransitioning : Boolean = false
|
||||
private set;
|
||||
var isInPictureInPicture : Boolean = false
|
||||
@@ -424,6 +425,7 @@ class VideoDetailFragment : MainFragment {
|
||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
||||
}
|
||||
isFullscreen = fullscreen;
|
||||
onFullscreenChanged.emit(isFullscreen);
|
||||
_view?.allowMotion = !fullscreen;
|
||||
}
|
||||
private fun changeOrientation(orientation: OrientationManager.Orientation) {
|
||||
|
||||
+81
-26
@@ -59,8 +59,11 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.DashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
@@ -99,6 +102,7 @@ import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
@@ -158,6 +162,7 @@ import java.time.OffsetDateTime
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@androidx.media3.common.util.UnstableApi
|
||||
class VideoDetailView : ConstraintLayout {
|
||||
private val TAG = "VideoDetailView"
|
||||
|
||||
@@ -1166,6 +1171,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
//@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||
_didTriggerDatasourceErrroCount = 0;
|
||||
_didTriggerDatasourceError = false;
|
||||
|
||||
if(newVideo && this.video?.url == videoDetail.url)
|
||||
return;
|
||||
@@ -1232,18 +1239,25 @@ class VideoDetailView : ConstraintLayout {
|
||||
}*/
|
||||
}
|
||||
try {
|
||||
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
||||
var tracker = video.getPlaybackTracker()
|
||||
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
||||
if(!StateApp.instance.privateMode) {
|
||||
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
||||
var tracker = video.getPlaybackTracker()
|
||||
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
||||
|
||||
if (tracker == null) {
|
||||
stopwatch.reset()
|
||||
tracker = StatePlatform.instance.getPlaybackTracker(video.url);
|
||||
Logger.i(TAG, "StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
||||
if (tracker == null) {
|
||||
stopwatch.reset()
|
||||
tracker = StatePlatform.instance.getPlaybackTracker(video.url);
|
||||
Logger.i(
|
||||
TAG,
|
||||
"StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms"
|
||||
)
|
||||
}
|
||||
|
||||
if (me.video == video)
|
||||
me._playbackTracker = tracker;
|
||||
}
|
||||
|
||||
if(me.video == video)
|
||||
me._playbackTracker = tracker;
|
||||
else if(me.video == video)
|
||||
me._playbackTracker = null;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Playback tracker failed", ex);
|
||||
@@ -1447,6 +1461,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||
StatePlayer.instance.setCurrentlyPlaying(video);
|
||||
|
||||
_liveChat?.stop();
|
||||
_liveChat = null;
|
||||
if(video.isLive && video.live != null) {
|
||||
loadLiveChat(video);
|
||||
}
|
||||
@@ -1643,6 +1659,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private var _didTriggerDatasourceErrroCount = 0;
|
||||
private var _didTriggerDatasourceError = false;
|
||||
private fun onDataSourceError(exception: Throwable) {
|
||||
Logger.e(TAG, "onDataSourceError", exception);
|
||||
@@ -1652,26 +1669,49 @@ class VideoDetailView : ConstraintLayout {
|
||||
return;
|
||||
val config = currentVideo.sourceConfig;
|
||||
|
||||
if(!_didTriggerDatasourceError) {
|
||||
if(_didTriggerDatasourceErrroCount <= 3) {
|
||||
_didTriggerDatasourceError = true;
|
||||
_didTriggerDatasourceErrroCount++;
|
||||
|
||||
UIDialogs.toast("Block detected, attempting bypass");
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||
val previousVideoSource = _lastVideoSource;
|
||||
val previousAudioSource = _lastAudioSource;
|
||||
|
||||
if(newDetails is IPlatformVideoDetails) {
|
||||
val newVideoSource = if(previousVideoSource != null)
|
||||
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
|
||||
else null;
|
||||
val newAudioSource = if(previousAudioSource != null)
|
||||
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong());
|
||||
else null;
|
||||
withContext(Dispatchers.Main) {
|
||||
video = newDetails;
|
||||
_player.setSource(newVideoSource, newAudioSource, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(_didTriggerDatasourceErrroCount > 3) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
||||
context.getString(R.string.media_error),
|
||||
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
||||
null,
|
||||
0,
|
||||
UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }),
|
||||
UIDialogs.Action(context.getString(R.string.yes), {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StatePlatform.instance.reloadClient(context, config.id);
|
||||
reloadVideo();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to reload video.", e)
|
||||
}
|
||||
UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }),
|
||||
UIDialogs.Action(context.getString(R.string.yes), {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StatePlatform.instance.reloadClient(context, config.id);
|
||||
reloadVideo();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to reload video.", e)
|
||||
}
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1768,15 +1808,21 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
val bestVideoSources = videoSources?.map { it.height * it.width }
|
||||
val doDedup = false;
|
||||
|
||||
val bestVideoSources = if(doDedup) (videoSources?.map { it.height * it.width }
|
||||
?.distinct()
|
||||
?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) }
|
||||
?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource }))
|
||||
?.distinct()
|
||||
?.filter { it != null }
|
||||
?.toList() ?: listOf();
|
||||
?.toList() ?: listOf() else videoSources?.toList() ?: listOf()
|
||||
val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container };
|
||||
val bestAudioSources = audioSources
|
||||
val bestAudioSources = if(doDedup) audioSources
|
||||
?.filter { it.container == bestAudioContainer }
|
||||
?.toList() ?: listOf();
|
||||
?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource })
|
||||
?.distinct()
|
||||
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||
|
||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
||||
@@ -2304,6 +2350,15 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
updateTracker(positionMilliseconds, isPlaying, false);
|
||||
|
||||
if(StateDeveloper.instance.isPlaybackTesting) {
|
||||
if((positionMilliseconds > 1000 * 65 || positionMilliseconds > (video!!.duration * 1000 - 1000))) {
|
||||
StateDeveloper.instance.testPlayback();
|
||||
}
|
||||
else if(video!!.duration > 70 && positionMilliseconds < 10000) {
|
||||
handleSeek(55000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) {
|
||||
|
||||
@@ -127,7 +127,7 @@ class VideoHelper {
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource {
|
||||
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
|
||||
val urlToUse = videoSource.getVideoUrl();
|
||||
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
|
||||
videoSource.duration * 1000,
|
||||
@@ -145,10 +145,10 @@ class VideoHelper {
|
||||
);
|
||||
|
||||
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
|
||||
return DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
|
||||
return Pair(DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
|
||||
Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null);
|
||||
return@Resolver dataSpec;
|
||||
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build())
|
||||
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()), manifestConfig);
|
||||
}
|
||||
|
||||
fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata {
|
||||
|
||||
@@ -15,7 +15,9 @@ import android.media.AudioManager
|
||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||
import android.media.MediaMetadata
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.background.BackgroundWorker
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
@@ -56,6 +57,18 @@ class StateApp {
|
||||
|
||||
val sessionId = UUID.randomUUID().toString();
|
||||
|
||||
var privateMode: Boolean = false
|
||||
get(){
|
||||
return field;
|
||||
}
|
||||
private set(value) {
|
||||
field = value;
|
||||
}
|
||||
val privateModeChanged = Event1<Boolean>();
|
||||
fun setPrivacyMode(value: Boolean) {
|
||||
privateMode = value;
|
||||
privateModeChanged.emit(privateMode);
|
||||
}
|
||||
|
||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.SettingsDev
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailView
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/***
|
||||
@@ -23,6 +31,12 @@ class StateDeveloper {
|
||||
|
||||
var devProxy: DevProxySettings? = null;
|
||||
|
||||
var testState: String? = null;
|
||||
val isPlaybackTesting: Boolean get() {
|
||||
return SettingsDev.instance.developerMode && testState == "TestPlayback";
|
||||
};
|
||||
|
||||
|
||||
fun initializeDev(id: String) {
|
||||
currentDevID = id;
|
||||
synchronized(_devLogs) {
|
||||
@@ -135,6 +149,37 @@ class StateDeveloper {
|
||||
}
|
||||
|
||||
|
||||
private var homePager: IPager<IPlatformContent>? = null;
|
||||
private var pagerIndex = 0;
|
||||
fun testPlayback(){
|
||||
val mainActivity = if(StateApp.instance.isMainActive) StateApp.instance.context as MainActivity else return;
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
if(homePager == null)
|
||||
homePager = StatePlatform.instance.getHome();
|
||||
var pager = homePager ?: return@launch;
|
||||
pagerIndex++;
|
||||
val video = if(pager.getResults().size <= pagerIndex) {
|
||||
if(!pager.hasMorePages()) {
|
||||
homePager = StatePlatform.instance.getHome();
|
||||
pager = homePager as IPager<IPlatformContent>;
|
||||
}
|
||||
pager.nextPage();
|
||||
pagerIndex = 0;
|
||||
val results = pager.getResults();
|
||||
if(results.size <= 0)
|
||||
null;
|
||||
else
|
||||
results[0];
|
||||
}
|
||||
else
|
||||
pager.getResults()[pagerIndex];
|
||||
|
||||
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||
mainActivity.navigate(mainActivity._fragVideoDetail, video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEV_ID = "DEV";
|
||||
|
||||
@@ -152,6 +197,7 @@ class StateDeveloper {
|
||||
it._server?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
|
||||
@@ -96,6 +96,8 @@ class StateHistory {
|
||||
return historyIndex[url];
|
||||
}
|
||||
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
||||
if(StateApp.instance.privateMode)
|
||||
return null;
|
||||
val existing = historyIndex[video.url];
|
||||
var result: DBHistory.Index? = null;
|
||||
if(existing != null) {
|
||||
@@ -113,6 +115,19 @@ class StateHistory {
|
||||
return result;
|
||||
}
|
||||
|
||||
fun markAsWatched(video: IPlatformVideo) {
|
||||
try {
|
||||
val history = getHistoryByVideo(video, true, OffsetDateTime.now());
|
||||
if (history != null) {
|
||||
updateHistoryPosition(video, history, true, Math.max(1, video.duration - 1));
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to mark as watched", ex);
|
||||
UIDialogs.toast("Failed to mark as watched\n" + ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
fun removeHistory(url: String) {
|
||||
val hist = getHistoryIndexByUrl(url);
|
||||
if(hist != null)
|
||||
|
||||
@@ -93,6 +93,7 @@ class StatePlatform {
|
||||
private val _channelClientPool = PlatformMultiClientPool("Channels", 15); //Used primarily for subscription/background channel fetches
|
||||
private val _trackerClientPool = PlatformMultiClientPool("Trackers", 1); //Used exclusively for playback trackers
|
||||
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
|
||||
private val _privateClientPool = PlatformMultiClientPool("Private", 2, true); //Used primarily for calls if in incognito mode
|
||||
|
||||
|
||||
private val _icons : HashMap<String, ImageVariable> = HashMap();
|
||||
@@ -109,13 +110,24 @@ class StatePlatform {
|
||||
//Batched Requests
|
||||
private val _batchTaskGetVideoDetails: BatchedTaskHandler<String, IPlatformContentDetails> = BatchedTaskHandler<String, IPlatformContentDetails>(_scope,
|
||||
{ url ->
|
||||
|
||||
Logger.i(StatePlatform::class.java.name, "Fetching video details [${url}]");
|
||||
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
|
||||
_mainClientPool.getClientPooled(it).getContentDetails(url)
|
||||
} ?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
||||
if(!StateApp.instance.privateMode) {
|
||||
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
|
||||
_mainClientPool.getClientPooled(it).getContentDetails(url)
|
||||
}
|
||||
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "Fetching details with private client");
|
||||
_enabledClients.find { it.isContentDetailsUrl(url) }?.let {
|
||||
_privateClientPool.getClientPooled(it).getContentDetails(url)
|
||||
}
|
||||
?: throw NoPlatformClientException("No client enabled that supports this url ($url)");
|
||||
}
|
||||
},
|
||||
{
|
||||
if(!Settings.instance.browsing.videoCache)
|
||||
if(!Settings.instance.browsing.videoCache || StateApp.instance.privateMode)
|
||||
return@BatchedTaskHandler null;
|
||||
else {
|
||||
val cached = synchronized(_cache) { _cache.get(it); } ?: return@BatchedTaskHandler null;
|
||||
@@ -131,7 +143,7 @@ class StatePlatform {
|
||||
}
|
||||
},
|
||||
{ para, result ->
|
||||
if(!Settings.instance.browsing.videoCache || (result is IPlatformVideo && result.isLive))
|
||||
if(!Settings.instance.browsing.videoCache || (result is IPlatformVideo && result.isLive) || StateApp.instance.privateMode)
|
||||
return@BatchedTaskHandler
|
||||
else {
|
||||
Logger.i(TAG, "Caching [${para}]");
|
||||
@@ -871,7 +883,10 @@ class StatePlatform {
|
||||
if(!client.capabilities.hasGetComments)
|
||||
return EmptyPager();
|
||||
|
||||
return client.fromPool(_mainClientPool).getComments(url);
|
||||
if(!StateApp.instance.privateMode)
|
||||
return client.fromPool(_mainClientPool).getComments(url);
|
||||
else
|
||||
return client.fromPool(_privateClientPool).getComments(url);
|
||||
}
|
||||
fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
||||
Logger.i(TAG, "Platform - getSubComments");
|
||||
@@ -882,7 +897,11 @@ class StatePlatform {
|
||||
fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? {
|
||||
Logger.i(TAG, "Platform - getLiveChat");
|
||||
var client = getContentClient(url);
|
||||
return client.fromPool(_liveEventClientPool).getLiveEvents(url);
|
||||
|
||||
if(!StateApp.instance.privateMode)
|
||||
return client.fromPool(_liveEventClientPool).getLiveEvents(url);
|
||||
else
|
||||
return client.fromPool(_privateClientPool).getLiveEvents(url);
|
||||
}
|
||||
fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? {
|
||||
Logger.i(TAG, "Platform - getLiveChat");
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider
|
||||
@@ -26,6 +27,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||
@@ -36,17 +38,21 @@ import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlSource
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.google.gson.Gson
|
||||
import getHttpDataSourceFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -68,6 +74,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
private set;
|
||||
|
||||
private var _lastVideoMediaSource: MediaSource? = null;
|
||||
private var _lastGeneratedDash: String? = null;
|
||||
private var _lastAudioMediaSource: MediaSource? = null;
|
||||
private var _lastSubtitleMediaSource: MediaSource? = null;
|
||||
private var _shouldPlaybackRestartOnConnectivity: Boolean = false;
|
||||
@@ -375,6 +382,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
||||
|
||||
private fun swapSourceInternal(videoSource: IVideoSource?) {
|
||||
_lastGeneratedDash = null;
|
||||
when(videoSource) {
|
||||
is LocalVideoSource -> swapVideoSourceLocal(videoSource);
|
||||
is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource);
|
||||
@@ -415,7 +423,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
if(videoSource.hasItag) {
|
||||
//Temporary workaround for Youtube
|
||||
try {
|
||||
_lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
|
||||
val results = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
|
||||
_lastGeneratedDash = results.second;
|
||||
_lastVideoMediaSource = results.first;
|
||||
return;
|
||||
}
|
||||
//If it fails to create the dash workaround, fallback to standard progressive
|
||||
@@ -635,6 +645,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
||||
when (error.errorCode) {
|
||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||
Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}");
|
||||
if(error.cause is HttpDataSource.InvalidResponseCodeException) {
|
||||
val cause = error.cause as HttpDataSource.InvalidResponseCodeException
|
||||
|
||||
Logger.w(TAG, null) {
|
||||
"ERROR BAD HTTP ${cause.responseCode},\n" +
|
||||
"Video Source: ${lastVideoSource?.toString()}\n" +
|
||||
"Audio Source: ${lastAudioSource?.toString()}\n" +
|
||||
"Dash: ${_lastGeneratedDash}"
|
||||
};
|
||||
}
|
||||
onDatasourceError.emit(error);
|
||||
}
|
||||
//PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
||||
|
||||
+6
@@ -25,6 +25,8 @@ import androidx.media3.datasource.HttpDataSource;
|
||||
import androidx.media3.datasource.HttpUtil;
|
||||
import androidx.media3.datasource.TransferListener;
|
||||
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject;
|
||||
import com.futo.platformplayer.logging.Logger;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ForwardingMap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -45,6 +47,8 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import kotlinx.serialization.json.Json;
|
||||
|
||||
/*
|
||||
* Based on the default ExoPlayer DefaultHttpDataSource
|
||||
*/
|
||||
@@ -582,6 +586,8 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
requestHeaders = result.getHeaders();
|
||||
}
|
||||
|
||||
Logger.Companion.v("JSHttpDataSource", "DataSource REQ: " + requestUrl + "\nHEADERS: [" + V8RemoteObject.Companion.getGsonStandard().toJson(requestHeaders)+ "]", null);
|
||||
|
||||
HttpURLConnection connection = openConnection(new URL(requestUrl));
|
||||
connection.setConnectTimeout(connectTimeoutMillis);
|
||||
connection.setReadTimeout(readTimeoutMillis);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#CC111111" />
|
||||
<corners android:radius="100dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M450,879Q372,873 304.5,840Q237,807 187,753.5Q137,700 108.5,629.5Q80,559 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,485 880,489.5Q880,494 880,499Q863,488 840.5,477.5Q818,467 799,460Q791,334 699.5,247Q608,160 480,160Q424,160 374.5,178Q325,196 284,228L529,473Q510,481 492.5,491.5Q475,502 458,514L228,284Q196,325 178,374.5Q160,424 160,480Q160,579 213.5,657.5Q267,736 352,773Q370,801 397,830Q424,859 450,879ZM680,800Q739,800 789.5,773Q840,746 870,700Q840,654 789.5,627Q739,600 680,600Q621,600 570.5,627Q520,654 490,700Q520,746 570.5,773Q621,800 680,800ZM680,880Q584,880 508.5,829.5Q433,779 400,700Q433,621 508.5,570.5Q584,520 680,520Q776,520 851.5,570.5Q927,621 960,700Q927,779 851.5,829.5Q776,880 680,880ZM680,760Q655,760 637.5,742.5Q620,725 620,700Q620,675 637.5,657.5Q655,640 680,640Q705,640 722.5,657.5Q740,675 740,700Q740,725 722.5,742.5Q705,760 680,760Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#635DAC"
|
||||
android:pathData="M450,879Q372,873 304.5,840Q237,807 187,753.5Q137,700 108.5,629.5Q80,559 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,485 880,489.5Q880,494 880,499Q863,488 840.5,477.5Q818,467 799,460Q791,334 699.5,247Q608,160 480,160Q424,160 374.5,178Q325,196 284,228L529,473Q510,481 492.5,491.5Q475,502 458,514L228,284Q196,325 178,374.5Q160,424 160,480Q160,579 213.5,657.5Q267,736 352,773Q370,801 397,830Q424,859 450,879ZM680,800Q739,800 789.5,773Q840,746 870,700Q840,654 789.5,627Q739,600 680,600Q621,600 570.5,627Q520,654 490,700Q520,746 570.5,773Q621,800 680,800ZM680,880Q584,880 508.5,829.5Q433,779 400,700Q433,621 508.5,570.5Q584,520 680,520Q776,520 851.5,570.5Q927,621 960,700Q927,779 851.5,829.5Q776,880 680,880ZM680,760Q655,760 637.5,742.5Q620,725 620,700Q620,675 637.5,657.5Q655,640 680,640Q705,640 722.5,657.5Q740,675 740,700Q740,725 722.5,742.5Q705,760 680,760Z"/>
|
||||
</vector>
|
||||
@@ -70,6 +70,21 @@
|
||||
android:visibility="gone"
|
||||
android:elevation="15dp">
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/incognito_button"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_disabled_visible_purple"
|
||||
android:background="@drawable/background_button_round_black"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="visible"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:elevation="50dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/toast_view" />
|
||||
|
||||
<com.futo.platformplayer.views.ToastView
|
||||
android:id="@+id/toast_view"
|
||||
android:layout_width="match_parent"
|
||||
@@ -79,4 +94,5 @@
|
||||
app:layout_constraintLeft_toLeftOf="@id/fragment_main"
|
||||
app:layout_constraintRight_toRightOf="@id/fragment_main"
|
||||
app:layout_constraintBottom_toBottomOf="@id/fragment_main" />
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
@@ -6,6 +6,17 @@
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/app_icon"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/foreground" />
|
||||
|
||||
<!--
|
||||
<ImageButton
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
@@ -13,13 +24,19 @@
|
||||
android:layout_marginEnd="4dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_construction" />
|
||||
-->
|
||||
|
||||
<!--<ImageButton
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingRight="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_futo_logo_text" />-->
|
||||
android:textSize="22dp"
|
||||
android:layout_marginTop="-2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="Grayjay"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"/>
|
||||
<!--
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
@@ -42,6 +59,8 @@
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="-8dp"/>
|
||||
</LinearLayout>
|
||||
-->
|
||||
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -15,13 +15,6 @@
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/foreground" />
|
||||
|
||||
<!--<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingRight="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_futo_logo_text" />-->
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -34,30 +27,6 @@
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
<!--
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="15dp"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="@string/under"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="3dp" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="20dp"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="@string/construction"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="-8dp"/>
|
||||
</LinearLayout>-->
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="add_to">Add to</string>
|
||||
<string name="lorem_ipsum" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</string>
|
||||
<string name="add_to_queue">Add to queue</string>
|
||||
<string name="add_to_history">Add to history</string>
|
||||
<string name="general">General</string>
|
||||
<string name="channel">Channel</string>
|
||||
<string name="home">Home</string>
|
||||
@@ -25,6 +26,7 @@
|
||||
<string name="sources">Sources</string>
|
||||
<string name="buy">Buy</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="privacy_mode">Privacy Mode</string>
|
||||
<string name="the_top_source_will_be_considered_primary">The top source will be considered primary</string>
|
||||
<string name="defaults">Defaults</string>
|
||||
<string name="home_screen">Home Screen</string>
|
||||
@@ -66,6 +68,8 @@
|
||||
<string name="enabled">Enabled</string>
|
||||
<string name="keep_screen_on">Keep screen on</string>
|
||||
<string name="keep_screen_on_while_casting">Keep screen on while casting</string>
|
||||
<string name="always_proxy_requests">Always proxy requests</string>
|
||||
<string name="always_proxy_requests_description">Always proxy requests when casting data through the device.</string>
|
||||
<string name="discover">Discover</string>
|
||||
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
|
||||
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
||||
@@ -475,6 +479,8 @@
|
||||
<string name="removes_all_subscriptions">Removes all subscriptions</string>
|
||||
<string name="settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities">Settings related to development server, be careful as it may open your phone to security vulnerabilities</string>
|
||||
<string name="start_server">Start Server</string>
|
||||
<string name="test_playback">Test Playback</string>
|
||||
<string name="test_playback_desc">Keeps playing videos</string>
|
||||
<string name="subscriptions_cache_5000">Subscriptions Cache 5000</string>
|
||||
<string name="history_cache_100">History Cache 100</string>
|
||||
<string name="start_server_on_boot">Start Server on boot</string>
|
||||
|
||||
Submodule app/src/stable/assets/sources/bilibili updated: b518be4dd5...2c279cb123
Submodule app/src/stable/assets/sources/odysee updated: 79c7542409...b8ceab3e57
Submodule app/src/stable/assets/sources/spotify updated: 4e826dcb6a...c700081466
Submodule app/src/stable/assets/sources/youtube updated: b32f7ba26e...546d862342
Submodule app/src/unstable/assets/sources/bilibili updated: b518be4dd5...2c279cb123
Submodule app/src/unstable/assets/sources/odysee updated: 79c7542409...b8ceab3e57
Submodule app/src/unstable/assets/sources/spotify updated: 4e826dcb6a...c700081466
Submodule app/src/unstable/assets/sources/youtube updated: b32f7ba26e...546d862342
@@ -70,7 +70,7 @@ A feed object representing a community post with text, and optionally images.
|
||||
|
||||
*Usage:*
|
||||
```javascript
|
||||
new PlatformPost{
|
||||
new PlatformPost({
|
||||
id: new PlatformID(config.name, item?.id, config.id),
|
||||
name: item?.attributes?.title,
|
||||
author: getPlatformAuthorLink(item, context),
|
||||
|
||||
Reference in New Issue
Block a user