Compare commits

...

39 Commits

Author SHA1 Message Date
Kelvin 72efb21439 Mark as watched action 2024-07-17 21:12:24 +02:00
Kelvin aa8790ebdb Remove mediasession interval 2024-07-17 20:11:56 +02:00
Kelvin 6d491052ee Invert privatemode boolean 2024-07-17 19:56:38 +02:00
Kelvin 87ff4691ce Merge branch 'playback-experiment' into 'master'
403 Bypass & Privacy mode

See merge request videostreaming/grayjay!26
2024-07-17 17:32:54 +00:00
Kelvin 34d76e79ed Mandatory host body and suffic for wildcard urls 2024-07-17 19:31:59 +02:00
Kelvin 31b43da96f Pass private client pool variable 2024-07-17 18:30:30 +02:00
Kelvin 0540e673e2 Remove under construction on sources 2024-07-17 18:24:59 +02:00
Kelvin 4e88a63809 Privacy mode, Handle 403s 2024-07-17 18:11:08 +02:00
Kelvin f7581f8a65 Block bypass attempts 2024-07-17 13:58:00 +02:00
Kelvin e87a1c079c Experimentation code 2024-07-17 01:37:53 +02:00
Kelvin 3f9477c246 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-07-16 20:18:54 +02:00
Kelvin 05ed1e188e Logging and refs 2024-07-16 20:18:46 +02:00
Koen f3d06e49f8 Added setting to always proxy requests for FCast. Added logging to print dash manifests. 2024-07-15 10:16:54 +02:00
Koen f9a4b68967 Updated submodules. 2024-07-14 15:39:57 +02:00
Kai DeLorenzo 3631cfe365 fix unstable api error 2024-07-05 12:06:32 -05:00
Koen 8766ae176e Updated Spotify and possible fix for media session being killed after 10 minutes of inactivity. 2024-07-04 10:49:59 +02:00
Kelvin 36b53d490f Exclude HLS and Dash from source dedup 2024-07-03 21:37:15 +02:00
Kelvin f9b8b812a4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-07-03 20:51:02 +02:00
Kelvin ac9eae5272 refs 2024-07-03 20:50:52 +02:00
Tom f270cc00d8 Merge branch 'github-issue-templates' into 'master'
Add issue template for bugs, docs, feature requests

See merge request videostreaming/grayjay!22
2024-07-02 20:41:49 +00:00
Kelvin a5a3f970da Merge 2024-06-30 18:03:29 +02:00
Kelvin 987c465bf8 Refs 2024-06-30 18:01:41 +02:00
Kai DeLorenzo cf3c766fd9 Update documentation_issue.yml 2024-06-28 15:34:55 +00:00
Kai DeLorenzo 7efafae432 Update feature_request.yml 2024-06-28 15:30:53 +00:00
Kai DeLorenzo 1b8f44dde3 Update bug_report.yml 2024-06-28 15:24:37 +00:00
Thomas Folbrecht 4d93246863 spelling 2024-06-27 14:53:27 -05:00
Thomas Folbrecht 0471886d9f forgot to commit changes to config 2024-06-27 14:31:52 -05:00
Thomas Folbrecht 266974b799 forgot to commit changes to config 2024-06-27 14:30:42 -05:00
Thomas Folbrecht c3663c67d7 add labeler, fix copy 2024-06-27 12:59:03 -05:00
Thomas Folbrecht 07bb23d10b fix license contact link 2024-06-26 20:04:32 -05:00
Thomas Folbrecht 749fc22c6b rm contact 2024-06-26 15:59:46 -05:00
Thomas Folbrecht 9f9a4e8298 Add issue template for bugs, docs, feature requests
points users to chat.futo.org for support
2024-06-26 15:42:12 -05:00
Kai DeLorenzo 39e7d64d3f remove save icon after saving 2024-06-26 15:03:01 -05:00
Kai DeLorenzo 35d8610c00 Update packageHttp.md 2024-06-26 17:01:25 +00:00
Koen bc550ae8f5 Removed exporting service. 2024-06-26 16:01:08 +02:00
Kai DeLorenzo c76ef7f19b Merge branch 'playlist-fixes' into 'master'
Playlist Fixes

See merge request videostreaming/grayjay!21
2024-06-25 15:35:46 +00:00
Kai DeLorenzo b7781264d3 changed playlist limit to 100
added save button to non-saved local playlists
2024-06-25 10:22:23 -05:00
Kai DeLorenzo 696e03941a pass through actions to local playlist and auto convert playlists with 20 or fewer videos 2024-06-24 13:00:58 -05:00
Kai DeLorenzo 4609a351dc don't save playlists that weren't explicitly copied
fixed exception failed to convert playlist job cancelled
2024-06-24 10:50:40 -05:00
58 changed files with 903 additions and 540 deletions
+80
View File
@@ -0,0 +1,80 @@
name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["bug", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for taking the time to fill out this bug report.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
## Filing a bug report
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you expect to happen?
placeholder: Tell us what you see!
validations:
required: true
- type: input
id: grayjay-version
attributes:
label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "242"
validations:
required: true
- type: dropdown
id: plugin
attributes:
label: What plugins are you seeing the problem on?
multiple: true
options:
- All
- Youtube
- BiliBili (CN)
- Twitch
- Odysee
- Rumble
- Kick
- PeerTube
- Patreon
- Nebula
- SoundCloud
- Other
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Plugin Version
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12"
- type: checkboxes
id: login
attributes:
label: When do you experience the issue?
options:
- label: While logged in
- label: While logged out
- label: N/A
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Need a Grayjay License?
url: https://pay.futo.org/api/PaymentPortal
about: Purchase a Grayjay license with FutoPay
- name: Plugin Building, Usage, or other Questions
url: https://chat.futo.org/#narrow/stream/46-Grayjay
about: Grayjays Community Chat
@@ -0,0 +1,63 @@
name: Documentation Issue
description: Report an issue or suggest a change in the documentation.
labels: ["documentation", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a documentation change request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
- type: textarea
id: grayjay-affected-pages
attributes:
label: Affected Pages
description: |
Link to or describe the pages relevant to your documentation change request.
placeholder:
value:
validations:
required: false
- type: textarea
id: grayjay-problem
attributes:
label: What is the docs issue?
description: What problems or suggestions do you have about the documentation?
placeholder:
value:
validations:
required: true
- type: textarea
id: grayjay-proposal
attributes:
label: Proposal
description: What documentation changes would fix this issue and where would you expect to find them? Are one or more page headings unclear? Do one or more pages need additional context, examples, or warnings? Do we need a new page or section dedicated to a specific topic? Your ideas help us understand what you and other users need from our documentation and how we can improve the content.
placeholder:
value:
validations:
required: false
- type: textarea
id: grayjay-references
attributes:
label: References
description: |
Are there any other open or closed GitLab/GitHub issues related to the problem or solution you described? If so, list them below. For example:
```
- #6017
```
placeholder:
value:
validations:
required: false
- type: markdown
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
@@ -0,0 +1,58 @@
name: Feature Request
description: Suggest a new feature or other enhancement.
labels: ["enhancement", "new"]
body:
- type: markdown
attributes:
value: |
# Thank you for opening a feature request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
[External Contributions are closed at this time](https://github.com/tom-futo/grayjay-android/blob/master/CONTRIBUTION.md#contributing-to-core)
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
- type: textarea
id: grayjay-use-case
attributes:
label: Use Cases
description: |
In order to properly evaluate a feature request, it is necessary to understand the use cases for it. Please describe below the _end goal_ you are trying to achieve that has led you to request this feature. Please keep this section focused on the problem and not on the suggested solution.
placeholder:
value:
validations:
required: true
- type: textarea
id: grayjay-proposal
attributes:
label: Proposal
description: |
If you have an idea for a way to address the problem via a change to Grayjay features, please describe it below.
In this section, it's helpful to include specific examples of how what you are suggesting might look in the application, this allows us to understand the full picture of what you are proposing. If you're not sure of some details, don't worry! When we evaluate the feature request we may suggest modifications as necessary to work within the design constraints of the Grayjay Core Application.
placeholder:
value:
validations:
required: false
- type: textarea
id: grayjay-references
attributes:
label: References
description: |
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above or to the suggested solution? If so, please create a list below that mentions each of them. For example:
```
- #10
```
placeholder:
value:
validations:
required: false
- type: markdown
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
+34
View File
@@ -0,0 +1,34 @@
name: Issue labeler
on:
issues:
types: [ opened ]
permissions:
contents: read
jobs:
label-component:
runs-on: ubuntu-latest
permissions:
# required for all workflows
issues: write
steps:
- uses: actions/checkout@v3
- name: Parse issue form
uses: stefanbuck/github-issue-parser@v3
id: issue-parser
with:
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
- name: Set labels based on plugin field
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
with:
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
section: plugin
block-list: |
None
Other
token: ${{ secrets.GITHUB_TOKEN }}
-3
View File
@@ -41,9 +41,6 @@
<service android:name=".services.DownloadService"
android:enabled="true"
android:foregroundServiceType="dataSync" />
<service android:name=".services.ExportingService"
android:enabled="true"
android:foregroundServiceType="dataSync" />
<receiver android:name=".receivers.MediaControlReceiver" />
<receiver android:name=".receivers.AudioNoisyReceiver" />
@@ -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 {
@@ -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)
@@ -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()
}
}
@@ -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");
@@ -1,47 +1,37 @@
package com.futo.platformplayer.downloads
import android.content.Context
import android.net.Uri
import android.os.Environment
import androidx.documentfile.provider.DocumentFile
import com.arthenica.ffmpegkit.*
import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.constructs.Event1
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.LogCallback
import com.arthenica.ffmpegkit.ReturnCode
import com.arthenica.ffmpegkit.StatisticsCallback
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanBitrate
import kotlinx.coroutines.*
import java.io.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.OutputStream
import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException
@kotlinx.serialization.Serializable
class VideoExport {
var state: State = State.QUEUED;
var videoLocal: VideoLocal;
var videoSource: LocalVideoSource?;
var audioSource: LocalAudioSource?;
var subtitleSource: LocalSubtitleSource?;
var progress: Double = 0.0;
var isCancelled = false;
var error: String? = null;
@kotlinx.serialization.Transient
val onStateChanged = Event1<State>();
@kotlinx.serialization.Transient
val onProgressChanged = Event1<Double>();
fun changeState(newState: State) {
state = newState;
onStateChanged.emit(newState);
}
constructor(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
this.videoLocal = videoLocal;
this.videoSource = videoSource;
@@ -50,8 +40,6 @@ class VideoExport {
}
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
if(isCancelled) throw CancellationException("Export got cancelled");
val v = videoSource;
val a = audioSource;
val s = subtitleSource;
@@ -107,7 +95,6 @@ class VideoExport {
throw Exception("Cannot export when no audio or video source is set.");
}
onProgressChanged.emit(100.0);
return@coroutineScope outputFile;
}
@@ -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);
@@ -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
);
}
@@ -8,7 +8,7 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.R
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.logging.Logger
@@ -16,12 +16,13 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
import com.futo.platformplayer.views.items.ActiveDownloadItem
import com.futo.platformplayer.views.items.PlaylistDownloadItem
import com.futo.platformplayer.views.others.ProgressBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -64,16 +65,6 @@ class DownloadsFragment : MainFragment() {
}
}
};
StateDownloads.instance.onExportsChanged.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
try {
Logger.i(TAG, "Reloading UI for exports");
_view?.reloadUI()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to reload UI for exports", e)
}
}
};
}
override fun onPause() {
@@ -81,7 +72,6 @@ class DownloadsFragment : MainFragment() {
StateDownloads.instance.onDownloadsChanged.remove(this);
StateDownloads.instance.onDownloadedChanged.remove(this);
StateDownloads.instance.onExportsChanged.remove(this);
}
private class DownloadsView : LinearLayout {
@@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateApp
@@ -144,53 +145,62 @@ class PlaylistFragment : MainFragment() {
}
fun onShown(parameter: Any?) {
_taskLoadPlaylist.cancel();
_taskLoadPlaylist.cancel()
if (parameter is Playlist?) {
_playlist = parameter;
_url = null;
_playlist = parameter
_url = null
if(parameter != null) {
setName(parameter.name);
setVideos(parameter.videos, true);
setVideoCount(parameter.videos.size);
setButtonDownloadVisible(true);
setButtonEditVisible(true);
if (parameter != null) {
setName(parameter.name)
setVideos(parameter.videos, true)
setVideoCount(parameter.videos.size)
setButtonDownloadVisible(true)
setButtonEditVisible(true)
if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
_fragment.topBar?.assume<NavigationTopBarFragment>()
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
StatePlaylists.instance.playlistStore.save(parameter)
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
arrayListOf()
)
UIDialogs.toast("Playlist saved")
}))
}
} else {
setName(null);
setVideos(null, false);
setVideoCount(-1);
setButtonDownloadVisible(false);
setButtonEditVisible(false);
setName(null)
setVideos(null, false)
setVideoCount(-1)
setButtonDownloadVisible(false)
setButtonEditVisible(false)
}
//TODO: Do I have to remove the showConvertPlaylistButton(); button here?
} else if (parameter is IPlatformPlaylist) {
_playlist = null;
_url = parameter.url;
_playlist = null
_url = parameter.url
setVideoCount(parameter.videoCount);
setName(parameter.name);
setVideos(null, false);
setButtonDownloadVisible(false);
setButtonEditVisible(false);
setVideoCount(parameter.videoCount)
setName(parameter.name)
setVideos(null, false)
setButtonDownloadVisible(false)
setButtonEditVisible(false)
fetchPlaylist();
fetchPlaylist()
} else if (parameter is String) {
_playlist = null;
_url = parameter;
_playlist = null
_url = parameter
setName(null);
setVideos(null, false);
setVideoCount(-1);
setButtonDownloadVisible(false);
setButtonEditVisible(false);
setName(null)
setVideos(null, false)
setVideoCount(-1)
setButtonDownloadVisible(false)
setButtonEditVisible(false)
fetchPlaylist();
fetchPlaylist()
}
_playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download)
}
}
@@ -31,12 +31,16 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.VideoListEditorViewHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
enum class Action {
PLAY_ALL, SHUFFLE, PLAY, NONE
}
class RemotePlaylistFragment : MainFragment() {
override val isMainView : Boolean = true;
@@ -85,6 +89,8 @@ class RemotePlaylistFragment : MainFragment() {
private val _adapterVideos: InsertedViewAdapterWithLoader<VideoListEditorViewHolder>;
private val _scrollListener: RecyclerView.OnScrollListener
constructor(fragment: RemotePlaylistFragment, inflater: LayoutInflater) : super(inflater.context) {
inflater.inflate(R.layout.fragment_remote_playlist, this);
@@ -97,18 +103,25 @@ class RemotePlaylistFragment : MainFragment() {
_imageLoader = findViewById(R.id.image_loader);
_recyclerPlaylist = findViewById(R.id.recycler_playlist);
_llmPlaylist = LinearLayoutManager(context);
_adapterVideos = InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
_adapterVideos = InsertedViewAdapterWithLoader(context,
arrayListOf(),
arrayListOf(),
childCountGetter = { _videos.size },
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_videos[position], false); },
childViewHolderBinder = { viewHolder, position ->
viewHolder.bind(
_videos[position],
false
)
},
childViewHolderFactory = { viewGroup, _ ->
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.list_playlist, viewGroup, false);
val holder = VideoListEditorViewHolder(view, null);
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.list_playlist, viewGroup, false)
val holder = VideoListEditorViewHolder(view, null)
holder.onClick.subscribe {
showConvertConfirmationModal();
};
return@InsertedViewAdapterWithLoader holder;
}
);
convertPlaylist(false, Action.PLAY, holder.video)
}
return@InsertedViewAdapterWithLoader holder
})
_recyclerPlaylist.adapter = _adapterVideos;
_recyclerPlaylist.layoutManager = _llmPlaylist;
@@ -128,10 +141,10 @@ class RemotePlaylistFragment : MainFragment() {
};
buttonPlayAll.setOnClickListener {
showConvertConfirmationModal();
convertPlaylist(false, Action.PLAY_ALL);
};
buttonShuffle.setOnClickListener {
showConvertConfirmationModal();
convertPlaylist(false, Action.SHUFFLE);
};
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>(
@@ -253,48 +266,76 @@ class RemotePlaylistFragment : MainFragment() {
}
}
private fun showConvertConfirmationModal() {
val remotePlaylist = _remotePlaylist;
private fun convertPlaylist(
savePlaylist: Boolean, action: Action, video: IPlatformVideo? = null
) {
val remotePlaylist = _remotePlaylist
if (remotePlaylist == null) {
UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading));
return;
UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading))
return
}
val c = context ?: return;
UIDialogs.showConfirmationDialog(c, "Conversion to local playlist is required for this action", {
setLoading(true);
val convert = {
setLoading(true)
UIDialogs.showDialogProgress(context) {
it.setText("Converting playlist..");
it.setProgress(0f);
it.setText("Converting playlist..")
it.setProgress(0f)
_fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val playlist = remotePlaylist.toPlaylist() { progress ->
val playlist = remotePlaylist.toPlaylist { progress ->
_fragment.lifecycleScope.launch(Dispatchers.Main) {
it.setProgress(progress.toDouble() / remotePlaylist.videoCount);
it.setProgress(progress.toDouble() / remotePlaylist.videoCount)
}
};
StatePlaylists.instance.playlistStore.save(playlist);
withContext(Dispatchers.Main) {
UIDialogs.toast("Playlist converted");
it.dismiss();
_fragment.navigate<PlaylistFragment>(playlist);
}
}
catch(ex: Throwable) {
UIDialogs.appToast("Failed to convert playlist.\n" + ex.message);
if (savePlaylist) {
StatePlaylists.instance.playlistStore.save(playlist)
}
_fragment.lifecycleScope.launch(Dispatchers.Main) {
UIDialogs.toast("Playlist converted")
it.dismiss()
_fragment.navigate<PlaylistFragment>(playlist)
when (action) {
Action.SHUFFLE -> StatePlayer.instance.setPlaylist(
playlist, focus = true, shuffle = true
)
Action.PLAY_ALL -> StatePlayer.instance.setPlaylist(
playlist, focus = true
)
Action.PLAY -> {
StatePlayer.instance.setPlaylist(
playlist, _videos.indexOf(video), true
)
}
Action.NONE -> {}
}
}
} catch (ex: Throwable) {
UIDialogs.appToast("Failed to convert playlist.\n" + ex.message)
}
}
}
});
}
if (remotePlaylist.videoCount > 100) {
val c = context ?: return
UIDialogs.showConfirmationDialog(
c, "Conversion to local playlist is required for this action", convert
)
} else {
convert()
}
}
private fun showConvertPlaylistButton() {
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
showConvertConfirmationModal();
convertPlaylist(true, Action.NONE);
}));
}
@@ -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) {
@@ -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 {
@@ -1,236 +0,0 @@
package com.futo.platformplayer.services
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.futo.platformplayer.R
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.downloads.VideoExport
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.share
import com.futo.platformplayer.states.Announcement
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.stores.FragmentedStorage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.OffsetDateTime
import java.util.UUID
class ExportingService : Service() {
private val TAG = "ExportingService";
private val EXPORT_NOTIF_ID = 4;
private val EXPORT_NOTIF_TAG = "export";
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
//Context
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
private var _notificationManager: NotificationManager? = null;
private var _notificationChannel: NotificationChannel? = null;
private val _client = ManagedHttpClient();
private var _started = false;
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Logger.i(TAG, "onStartCommand");
synchronized(this) {
if(_started)
return START_STICKY;
if(!FragmentedStorage.isInitialized) {
closeExportSession();
return START_NOT_STICKY;
}
_started = true;
}
setupNotificationRequirements();
_callOnStarted?.invoke(this);
_instance = this;
_scope.launch {
try {
doExporting();
}
catch(ex: Throwable) {
try {
StateAnnouncement.instance.registerAnnouncementSession(
Announcement(
"rootExportException",
"An root export service exception happened",
ex.message ?: "",
AnnouncementType.SESSION,
OffsetDateTime.now()
)
);
} catch(_: Throwable){}
}
};
return START_STICKY;
}
fun setupNotificationRequirements() {
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
this.enableVibration(false);
this.setSound(null, null);
};
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
}
override fun onCreate() {
Logger.i(TAG, "onCreate");
super.onCreate()
}
override fun onBind(p0: Intent?): IBinder? {
return null;
}
private suspend fun doExporting() {
Logger.i(TAG, "doExporting - Starting Exports");
val ignore = mutableListOf<VideoExport>();
var currentExport: VideoExport? = StateDownloads.instance.getExporting().firstOrNull();
while (currentExport != null)
{
try{
notifyExport(currentExport);
doExport(applicationContext, currentExport);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed export [${currentExport.videoLocal.name}]: ${ex.message}", ex);
currentExport.error = ex.message;
currentExport.changeState(VideoExport.State.ERROR);
ignore.add(currentExport);
//Give it a sec
Thread.sleep(500);
}
currentExport = StateDownloads.instance.getExporting().filter { !ignore.contains(it) }.firstOrNull();
}
Logger.i(TAG, "doExporting - Ending Exports");
stopService(this);
}
private suspend fun doExport(context: Context, export: VideoExport) {
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
export.changeState(VideoExport.State.EXPORTING);
var lastNotifyTime: Long = 0L;
val file = export.export(context) { progress ->
export.progress = progress;
val currentTime = System.currentTimeMillis();
if (currentTime - lastNotifyTime > 500) {
notifyExport(export);
lastNotifyTime = currentTime;
}
}
export.changeState(VideoExport.State.COMPLETED);
Logger.i(TAG, "Export [${export.videoLocal.name}] finished");
StateDownloads.instance.removeExport(export);
notifyExport(export);
withContext(Dispatchers.Main) {
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
file.share(this@ExportingService);
};
}
}
private fun notifyExport(export: VideoExport) {
val channel = _notificationChannel ?: return;
val bringUpIntent = Intent(this, MainActivity::class.java);
bringUpIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
bringUpIntent.action = "TAB";
bringUpIntent.putExtra("TAB", "Exports");
var builder = NotificationCompat.Builder(this, EXPORT_NOTIF_TAG)
.setSmallIcon(R.drawable.ic_export)
.setOngoing(true)
.setSilent(true)
.setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
.setContentTitle("${export.state}: ${export.videoLocal.name}")
.setContentText(export.getExportInfo())
.setProgress(100, (export.progress * 100).toInt(), export.progress == 0.0)
.setChannelId(channel.id)
val notif = builder.build();
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(EXPORT_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
} else {
startForeground(EXPORT_NOTIF_ID, notif);
}
}
fun closeExportSession() {
Logger.i(TAG, "closeExportSession");
stopForeground(STOP_FOREGROUND_REMOVE);
_notificationManager?.cancel(EXPORT_NOTIF_ID);
stopService();
_started = false;
super.stopSelf();
}
override fun onDestroy() {
Logger.i(TAG, "onDestroy");
_instance = null;
_scope.cancel("onDestroy");
super.onDestroy();
}
companion object {
private var _instance: ExportingService? = null;
private var _callOnStarted: ((ExportingService)->Unit)? = null;
@Synchronized
fun getOrCreateService(context: Context, handle: ((ExportingService)->Unit)? = null) {
if(!FragmentedStorage.isInitialized)
return;
if(_instance == null) {
_callOnStarted = handle;
val intent = Intent(context, ExportingService::class.java);
context.startForegroundService(intent);
}
else _instance?.let {
if(handle != null)
handle(it);
}
}
@Synchronized
fun getService() : ExportingService? {
return _instance;
}
@Synchronized
fun stopService(service: ExportingService? = null) {
(service ?: _instance)?.let {
if(_instance == it)
_instance = null;
it.closeExportSession();
}
}
}
}
@@ -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();
@@ -445,9 +458,6 @@ class StateApp {
DownloadService.getOrCreateService(context);
}
Logger.i(TAG, "MainApp Started: Check [Exports]");
StateDownloads.instance.checkForExportTodos();
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
@@ -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
@@ -1,13 +1,13 @@
package com.futo.platformplayer.states
import android.content.ContentResolver
import android.content.Context
import android.os.StatFs
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
@@ -27,10 +27,14 @@ import com.futo.platformplayer.models.DiskUsage
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.PlaylistDownloaded
import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.services.ExportingService
import com.futo.platformplayer.share
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.UUID
/***
* Used to maintain downloads
@@ -50,12 +54,8 @@ class StateDownloads {
private val _downloadPlaylists = FragmentedStorage.storeJson<PlaylistDownloadDescriptor>("playlistDownloads")
.load();
private val _exporting = FragmentedStorage.storeJson<VideoExport>("exporting")
.load();
private lateinit var _downloadedSet: HashSet<PlatformID>;
val onExportsChanged = Event0();
val onDownloadsChanged = Event0();
val onDownloadedChanged = Event0();
@@ -457,17 +457,6 @@ class StateDownloads {
}
}
try {
val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
for (export in exporting)
_exporting.delete(export);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to delete dangling export:", ex);
UIDialogs.toast("Failed to delete dangling export:\n" + ex);
}
return Pair(totalDeletedCount, totalDeleted);
}
@@ -475,66 +464,41 @@ class StateDownloads {
return _downloadsDirectory;
}
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
var lastNotifyTime = -1L;
UIDialogs.showDialogProgress(context) {
it.setText("Exporting content..");
it.setProgress(0f);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val export = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
try {
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
//Export
fun getExporting(): List<VideoExport> {
return _exporting.getItems();
}
fun checkForExportTodos() {
if(_exporting.hasItems()) {
StateApp.withContext {
ExportingService.getOrCreateService(it);
val file = export.export(context) { progress ->
val now = System.currentTimeMillis();
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
it.setProgress(progress);
lastNotifyTime = now;
}
}
withContext(Dispatchers.Main) {
it.setProgress(100.0f)
it.dismiss()
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
file.share(context);
};
}
} catch(ex: Throwable) {
Logger.e(TAG, "Failed export [${export.videoLocal.name}]: ${ex.message}", ex);
}
}
}
}
fun validateExport(export: VideoExport) {
if(_exporting.hasItem { it.videoLocal.url == export.videoLocal.url })
throw AlreadyQueuedException("Video [${export.videoLocal.name}] is already queued for export");
}
fun export(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, notify: Boolean = true) {
val shortName = if(videoLocal.name.length > 23)
videoLocal.name.substring(0, 20) + "...";
else
videoLocal.name;
val videoExport = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
try {
validateExport(videoExport);
_exporting.save(videoExport);
if(notify) {
UIDialogs.toast("Exporting [${shortName}]");
StateApp.withContext { ExportingService.getOrCreateService(it) };
onExportsChanged.emit();
}
}
catch (ex: AlreadyQueuedException) {
Logger.e(TAG, "File is already queued for export.", ex);
StateApp.withContext { ExportingService.getOrCreateService(it) };
}
catch(ex: Throwable) {
StateApp.withContext {
UIDialogs.showDialog(
it,
R.drawable.ic_error,
"Failed to start export due to:\n${ex.message}", null, null,
0,
UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY)
);
}
}
}
fun removeExport(export: VideoExport) {
_exporting.delete(export);
export.isCancelled = true;
onExportsChanged.emit();
}
companion object {
const val TAG = "StateDownloads";
@@ -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");
@@ -1,7 +1,6 @@
package com.futo.platformplayer.views
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.AttributeSet
import android.view.View
@@ -49,6 +48,7 @@ class MonetizationView : LinearLayout {
private val _taskLoadMerchandise = TaskHandler<String, List<StoreItem>>(StateApp.instance.scopeGetter, { url ->
val client = ManagedHttpClient();
Logger.i(TAG, "Loading https://storecache.grayjay.app/StoreData?url=$url")
val result = client.get("https://storecache.grayjay.app/StoreData?url=$url")
if (!result.isOk) {
throw Exception("Failed to retrieve store data.");
@@ -16,6 +16,8 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.adapters.AnyAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<VideoLocal>(
@@ -57,10 +59,14 @@ class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<
return@changeExternalDownloadDirectory;
}
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
}
};
} else {
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
}
}
}
}
@@ -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,
@@ -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>
+16
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>
@@ -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"
+6
View File
@@ -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>
+2 -2
View File
@@ -2,12 +2,12 @@
Package http is the main way for a plugin to make web requests, and is likely a package you will always need.
It offers several ways to make web requests as well as websocket connections.
Before you can use http you need to register it in your plugin config. See [Packages](_blank).
Before you can use http you need to register it in your plugin config. See [Packages](/app/src/main/java/com/futo/platformplayer/engine/packages).
## Basic Info
Underneath the http package by default exist two web clients. An authenticated client and a unauthenticated client.
The authenticated client has will apply headers and cookies if the user is logged in with your plugin.
See [Plugin Authentication](_blank).
See [Plugin Authentication](/docs/Authentication.md).
These two clients are always available even when the user is not logged in, meaning it behaves similar to the unauthenticated client and can safely use it either way.
>:warning: **Requests are synchronous**