Merge branch 'playback-experiment' into 'master'

403 Bypass & Privacy mode

See merge request videostreaming/grayjay!26
This commit is contained in:
Kelvin 2024-07-17 17:32:54 +00:00
commit 87ff4691ce
26 changed files with 361 additions and 87 deletions

View file

@ -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"));
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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)

View file

@ -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() {

View file

@ -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 {

View file

@ -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)

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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);

View file

@ -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
);
}

View file

@ -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) {

View file

@ -102,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
@ -1170,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;
@ -1236,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);
@ -1451,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);
}
@ -1647,6 +1659,7 @@ class VideoDetailView : ConstraintLayout {
}
}
private var _didTriggerDatasourceErrroCount = 0;
private var _didTriggerDatasourceError = false;
private fun onDataSourceError(exception: Throwable) {
Logger.e(TAG, "onDataSourceError", exception);
@ -1656,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)
);
}
}
}
@ -1772,19 +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 }
?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource })
?.distinct()
?.toList() ?: listOf();
?.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()
@ -2312,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) {

View file

@ -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();

View file

@ -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

View file

@ -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) {

View file

@ -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");

View file

@ -645,13 +645,14 @@ 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.v(TAG, null) {
Logger.w(TAG, null) {
"ERROR BAD HTTP ${cause.responseCode},\n" +
"Video Source: ${V8RemoteObject.gsonStandard.toJson(lastVideoSource)}\n" +
"Audio Source: ${V8RemoteObject.gsonStandard.toJson(lastAudioSource)}\n" +
"Video Source: ${lastVideoSource?.toString()}\n" +
"Audio Source: ${lastAudioSource?.toString()}\n" +
"Dash: ${_lastGeneratedDash}"
};
}

View file

@ -25,6 +25,7 @@ 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;
@ -46,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
*/
@ -583,7 +586,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
requestHeaders = result.getHeaders();
}
Logger.Companion.v("JSHttpDataSource", "DataSource REQ: " + requestUrl, null);
Logger.Companion.v("JSHttpDataSource", "DataSource REQ: " + requestUrl + "\nHEADERS: [" + V8RemoteObject.Companion.getGsonStandard().toJson(requestHeaders)+ "]", null);
HttpURLConnection connection = openConnection(new URL(requestUrl));
connection.setConnectTimeout(connectTimeoutMillis);

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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"

View file

@ -25,6 +25,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>
@ -477,6 +478,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>