Compare commits

...

7 Commits

Author SHA1 Message Date
Koen J f1ce0078fd Switch to rel.grayjay.app 2026-04-30 11:03:44 +02:00
Koen J 32de3649ef Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2026-04-30 10:10:17 +02:00
Koen J 1a301236da Changed background download to default. 2026-04-30 10:04:44 +02:00
Koen J c695379885 Fix for casting downloaded videos (UMP, requires redownloading). 2026-04-29 16:53:24 +02:00
Koen J 73466892f7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2026-04-29 13:13:37 +02:00
Koen J bb8a9d4dd7 Fixed notification system issues and plugin auto update. 2026-04-29 13:07:14 +02:00
Koen J 43ed2b16ab Hide browser interop warning. 2026-04-29 11:23:19 +02:00
11 changed files with 126 additions and 41 deletions
+3
View File
@@ -1,3 +1,6 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
buildAndDeployApkUnstable:
stage: build
script:
@@ -882,7 +882,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.background_download, FieldForm.TOGGLE, R.string.configure_if_background_download_should_be_used, 1)
//@DropdownFieldOptionsId(R.array.background_download)
var shouldBackgroundDownload: Boolean = false;
var shouldBackgroundDownload: Boolean = true;
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_updates_should_be_downloaded, 2)
@DropdownFieldOptionsId(R.array.when_download)
@@ -132,7 +132,7 @@ class UpdateDownloadService : Service() {
}
try {
performDownload(StateUpdate.APK_URL, partialFile, version, {
performDownload(StateUpdate.getApkUrl(version), partialFile, version, {
try {
if (announcement != null)
announcement?.setProgress(it);
@@ -170,12 +170,12 @@ class SourcePluginConfig(
"Unrestricted Http Header access",
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
))
if(packagesOptional.contains("Browser") || packages.contains("Browser")) {
/*if(packagesOptional.contains("Browser") || packages.contains("Browser")) {
list.add(Pair(
"Browser Interop",
"This plugin requires webbrowser interop. May access urls outside of the restricted urls. This will only work for official plugins and during development builds."
))
}
}*/
return list;
}
@@ -79,6 +79,7 @@ open class JSDashManifestRawSource(
_obj.getOrDefault<Boolean>(cfg, "canMerge", ctx, false) ?: false
override var streamMetaData: StreamMetaData? = null
var audioStreamMetaData: StreamMetaData? = null
private var _pregenerate: V8Deferred<String?>? = null
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
@@ -125,6 +126,14 @@ open class JSDashManifestRawSource(
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
val audioInitStart = _obj.getOrDefault<Int>(_config, "audioInitStart", "JSDashManifestRawSource", null) ?: 0;
val audioInitEnd = _obj.getOrDefault<Int>(_config, "audioInitEnd", "JSDashManifestRawSource", null) ?: 0;
val audioIndexStart = _obj.getOrDefault<Int>(_config, "audioIndexStart", "JSDashManifestRawSource", null) ?: 0;
val audioIndexEnd = _obj.getOrDefault<Int>(_config, "audioIndexEnd", "JSDashManifestRawSource", null) ?: 0;
if(audioInitEnd > 0 && audioIndexStart > 0 && audioIndexEnd > 0) {
audioStreamMetaData = StreamMetaData(audioInitStart, audioInitEnd, audioIndexStart, audioIndexEnd);
}
return@busy result.convert {
it.value
};
@@ -162,6 +171,14 @@ open class JSDashManifestRawSource(
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
val audioInitStart = _obj.getOrDefault<Int>(_config, "audioInitStart", "JSDashManifestRawSource", null) ?: 0;
val audioInitEnd = _obj.getOrDefault<Int>(_config, "audioInitEnd", "JSDashManifestRawSource", null) ?: 0;
val audioIndexStart = _obj.getOrDefault<Int>(_config, "audioIndexStart", "JSDashManifestRawSource", null) ?: 0;
val audioIndexEnd = _obj.getOrDefault<Int>(_config, "audioIndexEnd", "JSDashManifestRawSource", null) ?: 0;
if(audioInitEnd > 0 && audioIndexStart > 0 && audioIndexEnd > 0) {
audioStreamMetaData = StreamMetaData(audioInitStart, audioInitEnd, audioIndexStart, audioIndexEnd);
}
}
}
return result;
@@ -134,7 +134,7 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
var inputStream: InputStream? = null;
try {
val client = ManagedHttpClient();
val response = client.get(StateUpdate.APK_URL);
val response = client.get(StateUpdate.getApkUrl(_maxVersion));
if (response.isOk && response.body != null) {
inputStream = response.body.byteStream();
val dataLength = response.body.contentLength();
@@ -15,6 +15,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.google.android.material.button.MaterialButton
@@ -81,6 +82,8 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
Settings.instance.backup.didAskAutoBackup = true
Settings.instance.save()
StateAnnouncement.instance.deleteAnnouncement("backup")
UIDialogs.toast(context, context.getString(R.string.automatic_backup_enabled))
try {
StateBackup.startAutomaticBackup(true)
@@ -1462,6 +1462,9 @@ class VideoDownload {
if(localAudioSource != null && audioSourceToUse != null && audioSourceToUse is IStreamMetaDataSource)
localAudioSource.streamMetaData = (audioSourceToUse as IStreamMetaDataSource).streamMetaData;
if(localAudioSource != null && localAudioSource.streamMetaData == null && videoSourceToUse is JSDashManifestRawSource)
localAudioSource.streamMetaData = (videoSourceToUse as JSDashManifestRawSource).audioStreamMetaData;
if(existing != null) {
existing.videoSerialized = videoDetails!!;
if(localVideoSource != null) {
@@ -1,5 +1,6 @@
package com.futo.platformplayer.states
import android.content.Context
import android.view.View
import android.view.WindowManager
import com.futo.platformplayer.R
@@ -122,7 +123,7 @@ class StateAnnouncement {
//Special Announcements
fun registerPluginUpdate(oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): SessionAnnouncement {
val announcement = SessionAnnouncement(
"update-plugin-" + UUID.randomUUID().toString(),
"update-plugin-" + oldConfig.id + "-v" + newConfig.version,
"${newConfig.name} update v${newConfig.version} available!",
"An update is available to upgrade from ${oldConfig.version} to ${newConfig.version}.",
AnnouncementType.SESSION,
@@ -130,19 +131,62 @@ class StateAnnouncement {
null, null,oldConfig.id,
newConfig?.absoluteIconUrl?.let { ImageVariable.fromUrl(it) }
).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, oldConfig.id);
announcement.extraObj = newConfig;
registerAnnouncementSession(announcement);
return announcement;
}
fun registerPluginUpdated(newConfig: SourcePluginConfig) {
registerAnnouncementSession(SessionAnnouncement(
"updated-plugin-" + UUID.randomUUID().toString(),
val announcement = SessionAnnouncement(
"updated-plugin-" + newConfig.id + "-v" + newConfig.version,
"${newConfig.name} updated to v${newConfig.version}!",
"You have succesfully been updated to v${newConfig.version}.",
AnnouncementType.SESSION,
null, "updates", null, null,
null, null,null,
newConfig?.absoluteIconUrl?.let { ImageVariable.fromUrl(it) }
).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, newConfig.id));
).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, newConfig.id);
announcement.extraObj = newConfig;
registerAnnouncementSession(announcement);
}
fun tryAutoUpdatePlugin(oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) {
val context = StateApp.instance.contextOrNull;
if(context == null) {
registerPluginUpdate(oldConfig, newConfig);
return;
}
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val client = ManagedHttpClient();
client.setTimeout(10000);
val oldScript = StatePlugins.instance.getScript(oldConfig.id) ?: "";
val newScript = client.get(newConfig.absoluteScriptUrl)?.body?.string();
if(newScript.isNullOrEmpty()) {
Logger.w(TAG, "Auto-update for ${oldConfig.name}: no script returned, falling back to notification");
withContext(Dispatchers.Main) { registerPluginUpdate(oldConfig, newConfig); }
return@launch;
}
if(!oldConfig.isLowRiskUpdate(oldScript, newConfig, newScript)) {
withContext(Dispatchers.Main) { registerPluginUpdate(oldConfig, newConfig); }
return@launch;
}
StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, newConfig, newScript,
{ _: String, _: Double -> },
{ ex ->
if(ex == null) {
registerPluginUpdated(newConfig);
} else {
Logger.e(TAG, "Auto-update for ${newConfig.name} failed during install", ex);
UIDialogs.appToast("Update for ${newConfig.name} failed\n" + ex.message);
}
});
} catch(ex: Throwable) {
Logger.e(TAG, "Auto-update for ${oldConfig.name} failed", ex);
withContext(Dispatchers.Main) { registerPluginUpdate(oldConfig, newConfig); }
}
}
}
fun registerLoading(title: String, description: String, icon: ImageVariable? = null, customId: String? = null): SessionAnnouncement {
@@ -282,7 +326,7 @@ class StateAnnouncement {
when (actionId) {
ACTION_NEVER -> neverAnnouncement(item.id);
ACTION_SOMETHING -> actionSomething();
ACTION_CHANGELOG -> actionChangelog(actionData);
ACTION_CHANGELOG -> actionChangelog(item, actionData);
ACTION_UPDATE_PLUGIN -> actionUpdatePlugin(item.id, actionData);
}
}
@@ -314,27 +358,35 @@ class StateAnnouncement {
}
private fun actionChangelog(id: String?) {
if(id == null)
private fun actionChangelog(item: Announcement, id: String?) {
val context = StateApp.instance.contextOrNull ?: return;
val cached = (item as? SessionAnnouncement)?.extraObj as? SourcePluginConfig;
if(cached != null) {
showPluginChangelog(context, cached);
return;
}
StateApp.instance.contextOrNull?.let { context ->
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val plugin = StatePlugins.instance.getPlugin(id);
if (plugin == null)
return@launch
val update = StatePlugins.instance.checkForUpdates(plugin.config);
if(update == null)
return@launch;
if(id == null) return;
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
UIDialogs.showChangelogDialog(context, update.version, update.changelog!!.filterKeys { it.toIntOrNull() != null }
.mapKeys { it.key.toInt() }
.mapValues { update.getChangelogString(it.key.toString()) ?: "" });
}
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val plugin = StatePlugins.instance.getPlugin(id) ?: return@launch;
val update = StatePlugins.instance.checkForUpdates(plugin.config) ?: return@launch;
withContext(Dispatchers.Main) {
showPluginChangelog(context, update);
}
}
}
private fun showPluginChangelog(context: Context, config: SourcePluginConfig) {
if(config.changelog?.any() != true) {
UIDialogs.toast(context, "No changelog available");
return;
}
UIDialogs.showChangelogDialog(context, config.version, config.changelog!!.filterKeys { it.toIntOrNull() != null }
.mapKeys { it.key.toInt() }
.mapValues { config.getChangelogString(it.key.toString()) ?: "" });
}
private fun actionUpdatePlugin(notifId: String?, id: String?) {
if(id == null)
return;
@@ -363,7 +415,7 @@ class StateAnnouncement {
if(newScript.isNullOrEmpty())
throw IllegalStateException("No script found");
if(true || plugin.config.isLowRiskUpdate(script, update, newScript)) {
if(plugin.config.isLowRiskUpdate(script, update, newScript)) {
StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, update, newScript,
{ text: String, progress: Double -> },
{ ex ->
@@ -34,6 +34,7 @@ import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0
@@ -804,15 +805,24 @@ class StateApp {
if(StateHistory.instance.shouldMigrateLegacyHistory())
StateHistory.instance.migrateLegacyHistory();
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
scopeOrNull?.launch(Dispatchers.IO) {
val updateAvailable = StatePlugins.instance.checkForUpdates()
withContext(Dispatchers.Main) {
if (updateAvailable.isNotEmpty()) {
val toNotify = mutableListOf<Pair<SourcePluginConfig, SourcePluginConfig>>();
for(update in updateAvailable) {
if(!StatePlatform.instance.isClientEnabled(update.first.id))
continue;
val descriptor = StatePlugins.instance.getPlugin(update.first.id);
if(descriptor?.appSettings?.automaticUpdate == true)
StateAnnouncement.instance.tryAutoUpdatePlugin(update.first, update.second);
else
toNotify.add(update);
}
if(toNotify.isNotEmpty()) {
UIDialogs.appToast(
ToastView.Toast(updateAvailable
ToastView.Toast(toNotify
.map { " - " + it.first.name }
.joinToString("\n"),
true,
@@ -820,11 +830,8 @@ class StateApp {
"Plugin updates available"
));
for(update in updateAvailable)
if(StatePlatform.instance.isClientEnabled(update.first.id)) {
//UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
StateAnnouncement.instance.registerPluginUpdate(update.first, update.second);
}
for(update in toNotify)
StateAnnouncement.instance.registerPluginUpdate(update.first, update.second);
}
}
}
@@ -97,16 +97,16 @@ class StateUpdate {
throw Exception("App is not compatible. Supported ABIS: ${Build.SUPPORTED_ABIS.joinToString()}}.");
};
val VERSION_URL = if (BuildConfig.IS_UNSTABLE_BUILD) {
"https://releases.grayjay.app/version-unstable.txt"
"https://rel.grayjay.app/version-unstable.txt"
} else {
"https://releases.grayjay.app/version.txt"
"https://rel.grayjay.app/version.txt"
}
val APK_URL = if (BuildConfig.IS_UNSTABLE_BUILD) {
"https://releases.grayjay.app/app-$DESIRED_ABI-release-unstable.apk"
fun getApkUrl(version: Int): String = if (BuildConfig.IS_UNSTABLE_BUILD) {
"https://rel.grayjay.app/$version/app-$DESIRED_ABI-release-unstable.apk"
} else {
"https://releases.grayjay.app/app-$DESIRED_ABI-release.apk"
"https://rel.grayjay.app/$version/app-$DESIRED_ABI-release.apk"
}
val CHANGELOG_BASE_URL = "https://releases.grayjay.app/changelogs";
val CHANGELOG_BASE_URL = "https://rel.grayjay.app/changelogs";
fun getApkFile(context: Context, version: Int): File {
val dir = File(context.filesDir, "updates");