diff --git a/app/src/main/java/com/futo/platformplayer/SettingsDev.kt b/app/src/main/java/com/futo/platformplayer/SettingsDev.kt index aa5e4c94..76d7adf2 100644 --- a/app/src/main/java/com/futo/platformplayer/SettingsDev.kt +++ b/app/src/main/java/com/futo/platformplayer/SettingsDev.kt @@ -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")); + } } diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 90902f5e..f13d0cc0 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt index de2b9399..0119bf38 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt @@ -13,13 +13,15 @@ class PlatformClientPool { private val _pool: HashMap = hashMapOf(); private var _poolCounter = 0; private val _poolName: String?; + private val _privatePool: Boolean; var isDead: Boolean = false private set; val onDead = Event2(); - 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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/PlatformMultiClientPool.kt b/app/src/main/java/com/futo/platformplayer/api/media/PlatformMultiClientPool.kt index b77a4f35..a9fc3819 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/PlatformMultiClientPool.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/PlatformMultiClientPool.kt @@ -6,12 +6,14 @@ class PlatformMultiClientPool { private val _clientPools: HashMap = 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) diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt index f488929d..1d726dd5 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt @@ -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() { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 0c3288fb..8a6b40b0 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -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 { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt index 52914770..23a7a761 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt @@ -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? = null; private val _allowUrlsLower: List 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(json); if(obj.sourceUrl == null) diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt index 42eeffce..5130a2a4 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt index 0e86c2fc..7c77120c 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt index 44464b0d..c26885d9 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index 765bd20c..8855a89e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -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(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 ); } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt index 83b50a95..3c8a60da 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt @@ -39,6 +39,7 @@ class VideoDetailFragment : MainFragment { private var _view : SingleViewTouchableMotionLayout? = null; var isFullscreen : Boolean = false; + val onFullscreenChanged = Event1(); 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) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index c072e9a0..395e9d56 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -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) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index ae0f24a3..b4305f89 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -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(); + fun setPrivacyMode(value: Boolean) { + privateMode = value; + privateModeChanged.emit(privateMode); + } fun getExternalGeneralDirectory(context: Context): DocumentFile? { val generalUri = Settings.instance.storage.getStorageGeneralUri(); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt b/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt index 12904470..fcd16129 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt @@ -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? = 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; + } + 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 diff --git a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt index 4499ceda..4a13e0ed 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt @@ -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) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index 3e14ff2f..a92b4354 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -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 = HashMap(); @@ -109,13 +110,24 @@ class StatePlatform { //Batched Requests private val _batchTaskGetVideoDetails: BatchedTaskHandler = BatchedTaskHandler(_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 { Logger.i(TAG, "Platform - getSubComments"); @@ -882,7 +897,11 @@ class StatePlatform { fun getLiveEvents(url: String): IPager? { 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"); diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index 6889d911..21aca2a7 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -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}" }; } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java index bfd3d3e3..94b0fc99 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java +++ b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java @@ -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); diff --git a/app/src/main/res/drawable/background_button_round_black.xml b/app/src/main/res/drawable/background_button_round_black.xml new file mode 100644 index 00000000..caced27e --- /dev/null +++ b/app/src/main/res/drawable/background_button_round_black.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_disabled_visible.xml b/app/src/main/res/drawable/ic_disabled_visible.xml new file mode 100644 index 00000000..6e1ed6d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_disabled_visible.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_disabled_visible_purple.xml b/app/src/main/res/drawable/ic_disabled_visible_purple.xml new file mode 100644 index 00000000..eb3c4b6f --- /dev/null +++ b/app/src/main/res/drawable/ic_disabled_visible_purple.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 815d9f73..34f54a1f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -70,6 +70,21 @@ android:visibility="gone" android:elevation="15dp"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_top_bar.xml b/app/src/main/res/layout/fragment_add_top_bar.xml index cf072a7c..64465a11 100644 --- a/app/src/main/res/layout/fragment_add_top_bar.xml +++ b/app/src/main/res/layout/fragment_add_top_bar.xml @@ -6,6 +6,17 @@ android:orientation="horizontal" android:gravity="center_vertical"> + + + + - + 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"/> + + - - - - Sources Buy FAQ + Privacy Mode The top source will be considered primary Defaults Home Screen @@ -477,6 +478,8 @@ Removes all subscriptions Settings related to development server, be careful as it may open your phone to security vulnerabilities Start Server + Test Playback + Keeps playing videos Subscriptions Cache 5000 History Cache 100 Start Server on boot