Compare commits

...

29 Commits

Author SHA1 Message Date
Kelvin c6caa59a90 Refs 2025-07-04 18:00:48 +02:00
Kelvin 00e28b9ce0 Better raid messaging, loader autochange to indeterminate, refs 2025-07-04 17:27:35 +02:00
Kelvin 334f58979a Add missing file 2025-07-04 16:13:11 +02:00
Kelvin 940bf163da Progress bar color, refs 2025-07-04 15:58:46 +02:00
Kelvin 2bbe0e6133 Substitute v8 object calls to wrapper function 2025-07-04 15:24:19 +02:00
Kelvin 861f34a287 Refs 2025-07-04 15:02:01 +02:00
Koen J 86a4cf8d84 Implemented FutoVideoPlayer loader. 2025-07-04 08:09:24 +02:00
Kelvin 2c463dd5a1 Merge branch 'wip-async' into 'master'
Basic Async support

See merge request videostreaming/grayjay!132
2025-07-03 17:19:27 +00:00
Kelvin ed3820bec0 Finish async generates 2025-07-03 19:15:14 +02:00
Kelvin 542a7f212d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into wip-async 2025-07-03 19:05:38 +02:00
Kelvin 8fb0826d69 WIP rewrite generate to async 2025-07-03 19:05:13 +02:00
Koen J deeaa55f56 Changed default behavior for isOutgoing. 2025-07-03 14:40:27 +02:00
Koen J 5b954727a1 Implemented incoming and outgoing raids. 2025-07-03 14:30:15 +02:00
Koen fae77c1a63 Merge branch 'rgba-colors' into 'master'
RGBA colors.

See merge request videostreaming/grayjay!131
2025-07-03 07:44:19 +00:00
Kelvin b69402dfe9 WIP Async support for Android 2025-07-03 00:44:54 +02:00
Koen J 1f3e306a59 RGBA colors. 2025-07-02 17:59:13 +02:00
Koen J a9605118fb Clip to outline does not make sense for a ShapeableImageView. 2025-07-01 10:02:06 +02:00
Kelvin d22e918273 Missing catches 2025-06-26 16:17:17 +02:00
Kelvin bdcb94055a Refs 2025-06-26 15:13:41 +02:00
Kelvin d0644d39da Theoretical fix for networked file import 2025-06-26 15:01:18 +02:00
Kelvin 8f3f776e22 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-26 14:35:30 +02:00
Kelvin 548752e240 missing lock 2025-06-26 14:35:00 +02:00
Koen J 7f20250951 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-24 11:43:59 +02:00
Koen J 4d720b1d81 Fixed app freezing when exporting Polycentric Identity #2405 2025-06-24 11:43:40 +02:00
Kelvin K a8921a1aba Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-20 15:35:30 +02:00
Kelvin K edb9eda0a9 Improved locking 2025-06-20 15:35:02 +02:00
Koen J 3a81676447 Fixed crash #2389. 2025-06-20 10:47:10 +02:00
Koen J 03132ff77b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-19 11:23:28 +02:00
Koen J 49ddecdea4 Potential crashfix #2382. 2025-06-19 11:21:46 +02:00
55 changed files with 1390 additions and 113 deletions
@@ -0,0 +1,38 @@
package com.futo.platformplayer
import android.graphics.Color
import org.junit.Assert.assertEquals
import org.junit.Test
import toAndroidColor
class CSSColorTests {
@Test
fun test1() {
val androidHex = "#80336699"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#33669980"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor(),
)
}
@Test
fun test2() {
val androidHex = "#123ABC"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#123ABCFF"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor()
)
}
}
+2 -1
View File
@@ -707,11 +707,12 @@ class LiveEventViewCount extends LiveEvent {
}
}
class LiveEventRaid extends LiveEvent {
constructor(targetUrl, targetName, targetThumbnail) {
constructor(targetUrl, targetName, targetThumbnail, isOutgoing) {
super(100);
this.targetUrl = targetUrl;
this.targetName = targetName;
this.targetThumbnail = targetThumbnail;
this.isOutgoing = isOutgoing ?? true;
}
}
@@ -0,0 +1,319 @@
import kotlin.math.*
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
init {
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
"RGBA channels must be in [0,1]"
}
}
// -- RGB(A) channels stored 01 --
var r: Float = r.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var g: Float = g.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var b: Float = b.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var a: Float = a.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f) }
// -- Int views of RGBA 0255 --
var red: Int
get() = (r * 255).roundToInt()
set(v) { r = (v.coerceIn(0, 255) / 255f) }
var green: Int
get() = (g * 255).roundToInt()
set(v) { g = (v.coerceIn(0, 255) / 255f) }
var blue: Int
get() = (b * 255).roundToInt()
set(v) { b = (v.coerceIn(0, 255) / 255f) }
var alpha: Int
get() = (a * 255).roundToInt()
set(v) { a = (v.coerceIn(0, 255) / 255f) }
// -- HSLA storage & lazy recompute flags --
private var _h: Float = 0f
private var _s: Float = 0f
private var _l: Float = 0f
private var _hslDirty = true
/** Hue [0...360) */
var hue: Float
get() { computeHslIfNeeded(); return _h }
set(v) { setHsl(v, saturation, lightness) }
/** Saturation [0...1] */
var saturation: Float
get() { computeHslIfNeeded(); return _s }
set(v) { setHsl(hue, v, lightness) }
/** Lightness [0...1] */
var lightness: Float
get() { computeHslIfNeeded(); return _l }
set(v) { setHsl(hue, saturation, v) }
private fun computeHslIfNeeded() {
if (!_hslDirty) return
val max = max(max(r, g), b)
val min = min(min(r, g), b)
val d = max - min
_l = (max + min) / 2f
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
_h = when {
d == 0f -> 0f
max == r -> ((g - b) / d % 6f) * 60f
max == g -> (((b - r) / d) + 2f) * 60f
else -> (((r - g) / d) + 4f) * 60f
}.let { if (it < 0f) it + 360f else it }
_hslDirty = false
}
/**
* Set all three HSL channels at once.
* Hue in degrees [0...360), s/l [0...1].
*/
fun setHsl(h: Float, s: Float, l: Float) {
val hh = ((h % 360f) + 360f) % 360f
val cc = (1f - abs(2f * l - 1f)) * s
val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
val m = l - cc / 2f
val (rp, gp, bp) = when {
hh < 60f -> Triple(cc, x, 0f)
hh < 120f -> Triple(x, cc, 0f)
hh < 180f -> Triple(0f, cc, x)
hh < 240f -> Triple(0f, x, cc)
hh < 300f -> Triple(x, 0f, cc)
else -> Triple(cc, 0f, x)
}
r = rp + m; g = gp + m; b = bp + m
_h = hh; _s = s; _l = l; _hslDirty = false
}
/** Return 0xRRGGBBAA int */
fun toRgbaInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
}
/** Return 0xAARRGGBB int */
fun toArgbInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
}
// — Convenience modifiers (chainable) —
/** Lighten by fraction [0...1] */
fun lighten(fraction: Float): CSSColor = apply {
lightness = (lightness + fraction).coerceIn(0f, 1f)
}
/** Darken by fraction [0...1] */
fun darken(fraction: Float): CSSColor = apply {
lightness = (lightness - fraction).coerceIn(0f, 1f)
}
/** Increase saturation by fraction [0...1] */
fun saturate(fraction: Float): CSSColor = apply {
saturation = (saturation + fraction).coerceIn(0f, 1f)
}
/** Decrease saturation by fraction [0...1] */
fun desaturate(fraction: Float): CSSColor = apply {
saturation = (saturation - fraction).coerceIn(0f, 1f)
}
/** Rotate hue by degrees (can be negative) */
fun rotateHue(degrees: Float): CSSColor = apply {
hue = (hue + degrees) % 360f
}
companion object {
/** Create from Android 0xAARRGGBB */
@JvmStatic fun fromArgb(color: Int): CSSColor {
val a = ((color ushr 24) and 0xFF) / 255f
val r = ((color ushr 16) and 0xFF) / 255f
val g = ((color ushr 8) and 0xFF) / 255f
val b = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
/** Create from Android 0xRRGGBBAA */
@JvmStatic fun fromRgba(color: Int): CSSColor {
val r = ((color ushr 24) and 0xFF) / 255f
val g = ((color ushr 16) and 0xFF) / 255f
val b = ((color ushr 8) and 0xFF) / 255f
val a = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
@JvmStatic fun fromAndroidColor(color: Int): CSSColor {
return fromArgb(color)
}
private val NAMED_HEX = mapOf(
"aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
"aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
"bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
"blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
"burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
"chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
"cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
"darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
"darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
"darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
"darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
"darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
"darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
"darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
"dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
"firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
"fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
"gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
"green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
"honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
"indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
"lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
"lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
"lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
"lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
"lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
"lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
"lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
"linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
"mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
"mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
"mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
"midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
"moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
"oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
"orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
"palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
"palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
"peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
"powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
"red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
"saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
"seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
"silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
"slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
"springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
"teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
"turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
"white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
"yellowgreen" to "9ACD32"
)
private val NAMED: Map<String, Int> = NAMED_HEX
.mapValues { (_, hexRgb) ->
// parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
val rgb = hexRgb.toInt(16)
(rgb shl 8) or 0xFF
} + ("transparent" to 0x00000000)
private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
@JvmStatic
fun parseColor(s: String): CSSColor {
val str = s.trim()
// named
NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
// hex
HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
return parseHexPart(part)
}
// rgb/rgba
RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseRgbParts(it.split(',').map(String::trim))
}
// hsl/hsla
HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseHslParts(it.split(',').map(String::trim))
}
error("Cannot parse color: \"$s\"")
}
private fun parseHexPart(p: String): CSSColor {
// expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
val hex = when (p.length) {
3 -> p.map { "$it$it" }.joinToString("") + "FF"
4 -> p.map { "$it$it" }.joinToString("")
6 -> p + "FF"
8 -> p
else -> error("Invalid hex color: #$p")
}
val parsed = hex.toLong(16).toInt()
val alpha = (parsed and 0xFF) shl 24
val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
val argb = alpha or rgbOnly
return fromArgb(argb)
}
private fun parseRgbParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
// r/g/b: "128" → 128/255, "50%" → 0.5
fun channel(ch: String): Float =
if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
else ch.toFloat().coerceIn(0f, 255f) / 255f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
else a.toFloat().coerceIn(0f, 1f)
val r = channel(parts[0])
val g = channel(parts[1])
val b = channel(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(r, g, b, a)
}
private fun parseHslParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
fun hueOf(h: String): Float = when {
h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
else -> h.toFloat()
}
// for s and l you only ever see percentages
fun pct(p: String): Float =
p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) pct(a)
else a.toFloat().coerceIn(0f, 1f)
val h = hueOf(parts[0])
val s = pct(parts[1])
val l = pct(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
}
}
}
fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
fun CSSColor.toAndroidColor(): Int = toArgbInt()
@@ -2,12 +2,30 @@ package com.futo.platformplayer
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.*
import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.selects.SelectClause0
import kotlinx.coroutines.selects.SelectClause1
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
//V8
@@ -174,4 +192,137 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
map.put(prop, obj.getString(prop));
return map;
}
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
val latch = CountDownLatch(1);
var promiseResult: T? = null;
var promiseException: Throwable? = null;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
if(p0 is V8ValueError)
promiseException = ScriptExecutionException(plugin.config, p0.message);
else
promiseResult = p0 as T;
latch.countDown();
}
override fun onRejected(p0: V8Value?) {
promiseException = (NotImplementedError("onRejected promise not implemented.."));
latch.countDown();
}
override fun onCatch(p0: V8Value?) {
promiseException = (NotImplementedError("onCatch promise not implemented.."));
latch.countDown();
}
});
}
plugin.registerPromise(this) {
promiseException = CancellationException("Cancelled by system");
latch.countDown();
}
plugin.unbusy {
latch.await();
}
if(promiseException != null)
throw promiseException!!;
return promiseResult!!;
}
fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T> {
val underlyingDef = CompletableDeferred<T>();
val def = if(this.has("estDuration"))
V8Deferred(underlyingDef,
this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
else
V8Deferred<T>(underlyingDef);
if(def.estDuration > 0)
Logger.i("V8", "Promise with duration: [${def.estDuration}]");
val promise = this;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.complete(p0 as T);
}
override fun onRejected(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
}
override fun onCatch(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
}
});
}
plugin.registerPromise(promise) {
if(def.isActive)
def.cancel("Cancelled by system");
}
return def;
}
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
val newDef = CompletableDeferred<R>()
this.invokeOnCompletion {
if(it != null)
newDef.completeExceptionally(it);
else
newDef.complete(conversion(this@V8Deferred.getCompleted()));
}
return V8Deferred<R>(newDef, estDuration);
}
companion object {
fun <T, R> merge(scope: CoroutineScope, defs: List<V8Deferred<T>>, conversion: (result: List<T>)->R): V8Deferred<R> {
var amount = -1;
for(def in defs)
amount = Math.max(amount, def.estDuration);
val def = scope.async {
val results = defs.map { it.await() };
return@async conversion(results);
}
return V8Deferred(def, amount);
}
}
}
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result as T;
}
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!);
}
return V8Deferred(CompletableDeferred(result as T));
}
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result;
}
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
return result;
}
return V8Deferred(CompletableDeferred(result));
}
@@ -14,10 +14,12 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.ContentType
@@ -29,6 +31,9 @@ import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol
import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo
@@ -39,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView;
private lateinit var _loader: View
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -49,24 +55,47 @@ class PolycentricBackupActivity : AppCompatActivity() {
setContentView(R.layout.activity_polycentric_backup);
setNavigationBarColorAndIcons();
_buttonShare = findViewById(R.id.button_share);
_buttonCopy = findViewById(R.id.button_copy);
_imageQR = findViewById(R.id.image_qr);
_textQR = findViewById(R.id.text_qr);
_buttonShare = findViewById(R.id.button_share)
_buttonCopy = findViewById(R.id.button_copy)
_imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_qr)
_loader = findViewById(R.id.progress_loader)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish();
};
_exportBundle = createExportBundle();
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
try {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
_imageQR.setImageBitmap(qrCodeBitmap);
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
_imageQR.visibility = View.INVISIBLE;
_textQR.visibility = View.INVISIBLE;
lifecycleScope.launch {
try {
val pair = withContext(Dispatchers.IO) {
val bundle = createExportBundle()
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
).toInt()
val qr = generateQRCode(bundle, dimension, dimension)
Pair(bundle, qr)
}
_exportBundle = pair.first
_imageQR.setImageBitmap(pair.second)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
} catch (e: Exception) {
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
_imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
} finally {
_loader.visibility = View.GONE
}
}
_buttonShare.onClick.subscribe {
@@ -34,8 +34,10 @@ class PlatformClientPool {
isDead = true;
onDead.emit(parentClient, this);
for(clientPair in _pool) {
clientPair.key.disable();
synchronized(_pool) {
for (clientPair in _pool) {
clientPair.key.disable();
}
}
};
}
@@ -18,8 +18,7 @@ class LiveEventEmojis: IPlatformLiveEvent {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
obj.ensureIsBusy();
val contextName = "LiveEventEmojis"
return LiveEventEmojis(
obj.getOrThrow(config, "emojis", contextName));
return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
}
}
}
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
class LiveEventRaid: IPlatformLiveEvent {
@@ -11,11 +12,13 @@ class LiveEventRaid: IPlatformLiveEvent {
val targetName: String;
val targetThumbnail: String;
val targetUrl: String;
val isOutgoing: Boolean;
constructor(name: String, url: String, thumbnail: String) {
constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
this.targetName = name;
this.targetUrl = url;
this.targetThumbnail = thumbnail;
this.isOutgoing = isOutgoing;
}
companion object {
@@ -25,7 +28,8 @@ class LiveEventRaid: IPlatformLiveEvent {
return LiveEventRaid(
obj.getOrThrow(config, "targetName", contextName),
obj.getOrThrow(config, "targetUrl", contextName),
obj.getOrThrow(config, "targetThumbnail", contextName));
obj.getOrThrow(config, "targetThumbnail", contextName),
obj.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
}
}
}
@@ -632,7 +632,6 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
}
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.warnIfMainThread
abstract class JSPager<T> : IPager<T> {
@@ -49,7 +50,7 @@ abstract class JSPager<T> : IPager<T> {
val pluginV8 = plugin.getUnderlyingPlugin();
pluginV8.busy {
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invoke("nextPage", arrayOf<Any>());
pager.invokeV8("nextPage", arrayOf<Any>());
};
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.warnIfMainThread
@@ -57,7 +58,7 @@ class JSPlaybackTracker: IPlaybackTracker {
_client.busy {
if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeVoid("onInit", seconds);
_obj.invokeV8Void("onInit", seconds);
}
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true;
@@ -73,7 +74,7 @@ class JSPlaybackTracker: IPlaybackTracker {
else {
_client.busy {
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
_obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying);
_obj.invokeV8Void("onProgress", Math.floor(seconds), isPlaying);
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_lastRequest = System.currentTimeMillis();
}
@@ -86,7 +87,7 @@ class JSPlaybackTracker: IPlaybackTracker {
synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded");
_client.busy {
_obj.invokeVoid("onConcluded", -1);
_obj.invokeV8Void("onConcluded", -1);
}
}
}
@@ -14,6 +14,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.Serializable
@@ -55,7 +57,7 @@ class JSRequestExecutor {
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers, method, body);
_executor.invokeV8("executeRequest", url, headers, method, body);
} as V8Value;
}
else V8Plugin.catchScriptErrors<Any>(
@@ -63,7 +65,7 @@ class JSRequestExecutor {
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers, method, body);
_executor.invokeV8("executeRequest", url, headers, method, body);
} as V8Value;
try {
@@ -110,7 +112,7 @@ class JSRequestExecutor {
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
_executor.invokeV8("cleanup", null);
};
}
else V8Plugin.catchScriptErrors<Any>(
@@ -118,7 +120,7 @@ class JSRequestExecutor {
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
_executor.invokeV8("cleanup", null);
};
}
}
@@ -11,6 +11,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient;
@@ -40,7 +42,7 @@ class JSRequestModifier: IRequestModifier {
return _plugin.busy {
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers);
_modifier.invokeV8("modifyRequest", url, headers);
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers);
@@ -6,6 +6,8 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
override val licenseUri: String
@@ -25,7 +27,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
@@ -1,6 +1,8 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.V8Deferred
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.IVideoUrlSource
@@ -13,8 +15,13 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String;
@@ -50,6 +57,44 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
hasGenerate = _obj.has("generate");
}
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = null;
if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
}
return plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
return@busy result.convert {
it.value
};
}
}
override fun generate(): String? {
if(!hasGenerate)
return manifest;
@@ -63,24 +108,26 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
_obj.invokeString("generate");
_obj.invokeV8<V8ValueString>("generate").value;
}
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
_obj.invokeString("generate");
_obj.invokeV8<V8ValueString>("generate").value;
}
}
if(result != null){
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
}
}
return result;
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.V8Deferred
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
@@ -15,11 +16,18 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
interface IJSDashManifestRawSource {
val hasGenerate: Boolean;
var manifest: String?;
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
fun generate(): String?;
}
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
@@ -57,6 +65,45 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
hasGenerate = _obj.has("generate");
}
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = null;
if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
});
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
});
return plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
return@busy result.convert {
it.value
};
}
}
override open fun generate(): String? {
if(!hasGenerate)
return manifest;
@@ -68,7 +115,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
_obj.invokeString("generate");
_obj.invokeV8<V8ValueString>("generate").value;
}
});
}
@@ -76,17 +123,19 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
_obj.invokeString("generate");
_obj.invokeV8<V8ValueString>("generate").value;
}
});
if(result != null){
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
_plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
}
}
return result;
@@ -114,6 +163,32 @@ class JSDashManifestMergingRawSource(
override val priority: Boolean
get() = video.priority;
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
val videoDashDef = video.generateAsync(scope);
val audioDashDef = audio.generateAsync(scope);
return V8Deferred.merge(scope, listOf(videoDashDef, audioDashDef)) {
val (videoDash: String?, audioDash: String?) = it;
if (videoDash != null && audioDash == null) return@merge videoDash;
if (audioDash != null && videoDash == null) return@merge audioDash;
if (videoDash == null) return@merge null;
//TODO: Temporary simple solution..make more reliable version
var result: String? = null;
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
if (audioAdaptationSet != null) {
result = videoDash.replace(
"</AdaptationSet>",
"</AdaptationSet>\n" + audioAdaptationSet.value
)
} else
result = videoDash;
return@merge result;
};
}
override fun generate(): String? {
val videoDash = video.generate();
val audioDash = audio.generate();
@@ -9,6 +9,8 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
IDashManifestWidevineSource, JSSource {
@@ -45,7 +47,7 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
@@ -16,6 +16,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
@@ -64,7 +65,7 @@ abstract class JSSource {
return@isBusyWith null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
_obj.invoke("getRequestModifier", arrayOf<Any>());
_obj.invokeV8("getRequestModifier", arrayOf<Any>());
};
if (result !is V8ValueObject)
@@ -78,7 +79,7 @@ abstract class JSSource {
Logger.v("JSSource", "Request executor for [${type}] requesting");
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestExecutor", arrayOf<Any>());
_obj.invokeV8("getRequestExecutor", arrayOf<Any>());
};
Logger.v("JSSource", "Request executor for [${type}] received");
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
override val licenseUri: String
@@ -25,7 +26,7 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
_obj.invokeV8("getLicenseRequestExecutor", arrayOf<Any>())
}
if (result !is V8ValueObject)
@@ -6,12 +6,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Button
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
import com.futo.platformplayer.readBytes
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ImportOptionsDialog: AlertDialog {
private val _context: MainActivity;
@@ -41,8 +45,17 @@ class ImportOptionsDialog: AlertDialog {
_button_import_zip.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/zip") {
val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val zipBytes = it?.readBytes(context) ?: return@launch;
withContext(Dispatchers.Main) {
try {
StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
}
catch(ex: Throwable) {
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
}
}
}
};
}
_button_import_ezip.setOnClickListener {
@@ -51,17 +64,35 @@ class ImportOptionsDialog: AlertDialog {
_button_import_txt.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "text/plain") {
val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
val txt = String(txtBytes);
StateBackup.importTxt(_context, txt);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val txtBytes = it?.readBytes(context) ?: return@launch;
val txt = String(txtBytes);
withContext(Dispatchers.Main) {
try {
StateBackup.importTxt(_context, txt);
}
catch(ex: Throwable) {
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
}
}
}
};
}
_button_import_newpipe_subs.onClick.subscribe {
dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/json") {
val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
val json = String(jsonBytes);
StateBackup.importNewPipeSubs(_context, json);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val jsonBytes = it?.readBytes(context) ?: return@launch;
val json = String(jsonBytes);
withContext(Dispatchers.Main) {
try {
StateBackup.importNewPipeSubs(_context, json);
}
catch(ex: Throwable) {
UIDialogs.toast("Failed to import, invalid format?\n" + ex.message);
}
}
}
};
};
_button_import_platform.onClick.subscribe {
@@ -10,7 +10,9 @@ import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1
@@ -37,7 +39,15 @@ import com.futo.platformplayer.engine.packages.V8Package
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets
import com.futo.platformplayer.toList
import com.futo.platformplayer.toV8ValueBlocking
import com.futo.platformplayer.toV8ValueAsync
import com.futo.platformplayer.warnIfMainThread
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.cancel
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -48,6 +58,7 @@ class V8Plugin {
private val _clientAuth: ManagedHttpClient;
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
private val _promises = ConcurrentHashMap<V8ValuePromise, ((V8ValuePromise)->Unit)?>();
val httpClient: ManagedHttpClient get() = _client;
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
@@ -223,37 +234,144 @@ class V8Plugin {
Logger.i(TAG, "Plugin stopped");
onStopped.emit(this);
}
cancelAllPromises();
}
fun isThreadAlreadyBusy(): Boolean {
return _busyLock.isHeldByCurrentThread;
}
fun <T> busy(handle: ()->T): T {
_busyLock.lock();
try {
return handle();
}
finally {
_busyLock.unlock();
}
/*
_busyLock.withLock {
//Logger.i(TAG, "Entered busy: " + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString());
return handle();
}*/
}
fun <T> unbusy(handle: ()->T): T {
val wasLocked = isThreadAlreadyBusy();
if(!wasLocked)
return handle();
val lockCount = _busyLock.holdCount;
for(i in 1..lockCount)
_busyLock.unlock();
try {
Logger.w(TAG, "Unlocking V8 thread for [${config.name}] for a blocking resolve of a promise")
return handle();
}
finally {
Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise")
for(i in 1..lockCount)
_busyLock.lock();
}
}
fun execute(js: String) : V8Value {
return executeTyped<V8Value>(js);
}
suspend fun <T : V8Value> executeTypedAsync(js: String) : Deferred<T> {
warnIfMainThread("V8Plugin.executeTypedAsync");
if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js);
return withContext(IO) {
return@withContext busy {
try {
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
val result = catchScriptErrors<V8Value>("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute()
};
if (result is V8ValuePromise) {
return@busy result.toV8ValueAsync<T>(this@V8Plugin);
} else
return@busy CompletableDeferred(result as T);
}
catch(ex: Throwable) {
val def = CompletableDeferred<T>();
def.completeExceptionally(ex);
return@busy def;
}
}
}
}
fun <T : V8Value> executeTyped(js: String) : T {
warnIfMainThread("V8Plugin.executeTyped");
if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js);
return busy {
val result = busy {
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
return@busy catchScriptErrors("Plugin[${config.name}]", js) {
return@busy catchScriptErrors<V8Value>("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute()
};
};
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this@V8Plugin);
}
return result as T;
}
fun executeBoolean(js: String) : Boolean? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value } }
fun executeString(js: String) : String? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value } }
fun executeInteger(js: String) : Int? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value } }
fun <T: V8Value> handlePromise(result: V8ValuePromise): CompletableDeferred<T> {
val def = CompletableDeferred<T>();
result.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
resolvePromise(result);
def.complete(p0 as T);
}
override fun onRejected(p0: V8Value?) {
resolvePromise(result);
def.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
}
override fun onCatch(p0: V8Value?) {
resolvePromise(result);
def.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
}
});
registerPromise(result) {
if(def.isActive)
def.cancel("Cancelled by system");
}
return def;
}
fun registerPromise(promise: V8ValuePromise, onCancelled: ((V8ValuePromise)->Unit)? = null) {
Logger.v(TAG, "Promise registered for plugin [${config.name}]: ${promise.hashCode()}");
if (onCancelled != null) {
_promises.put(promise, onCancelled)
};
}
fun resolvePromise(promise: V8ValuePromise, cancelled: Boolean = false) {
Logger.v(TAG, "Promise resolved for plugin [${config.name}]: ${promise.hashCode()}");
val found = synchronized(_promises) {
val found = _promises.getOrDefault(promise, null);
_promises.remove(promise);
return@synchronized found;
};
if(found != null && cancelled)
found(promise);
}
fun cancelAllPromises(){
val promises = _promises.keys().toList();
for(key in promises) {
try {
resolvePromise(key, true);
}
catch(ex: Throwable) {}
}
}
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
//TODO: Auto get all package types?
return when(packageName) {
@@ -84,7 +84,8 @@ class PackageBridge : V8Package {
fun supportedFeatures(): Array<String> {
return arrayOf(
"ReloadRequiredException",
"HttpBatchClient"
"HttpBatchClient",
"Async"
);
}
@@ -130,9 +131,12 @@ class PackageBridge : V8Package {
}
timeoutMap.remove(id);
try {
Logger.w(TAG, "setTimeout before busy (${timeout}): ${_plugin.isBusy}");
_plugin.busy {
Logger.w(TAG, "setTimeout in busy");
if (!_plugin.isStopped)
funcClone.callVoid(null, arrayOf<Any>());
Logger.w(TAG, "setTimeout after");
}
} catch (ex: Throwable) {
Logger.e(TAG, "Failed timeout callback", ex);
@@ -17,6 +17,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.internal.IV8Convertable
import com.futo.platformplayer.engine.internal.V8BindObject
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger
import java.net.SocketTimeoutException
import java.util.concurrent.ForkJoinPool
@@ -668,7 +669,7 @@ class PackageHttp: V8Package {
if(hasOpen && _listeners?.isClosed != true) {
try {
_package._plugin.busy {
_listeners?.invokeVoid("open", arrayOf<Any>());
_listeners?.invokeV8Void("open", arrayOf<Any>());
}
}
catch(ex: Throwable){
@@ -680,7 +681,7 @@ class PackageHttp: V8Package {
if(hasMessage && _listeners?.isClosed != true) {
try {
_package._plugin.busy {
_listeners?.invokeVoid("message", msg);
_listeners?.invokeV8Void("message", msg);
}
}
catch(ex: Throwable) {}
@@ -691,7 +692,7 @@ class PackageHttp: V8Package {
{
try {
_package._plugin.busy {
_listeners?.invokeVoid("closing", code, reason);
_listeners?.invokeV8Void("closing", code, reason);
}
}
catch(ex: Throwable){
@@ -704,7 +705,7 @@ class PackageHttp: V8Package {
if(hasClosed && _listeners?.isClosed != true) {
try {
_package._plugin.busy {
_listeners?.invokeVoid("closed", code, reason);
_listeners?.invokeV8Void("closed", code, reason);
}
}
catch(ex: Throwable){
@@ -722,7 +723,7 @@ class PackageHttp: V8Package {
if(hasFailure && _listeners?.isClosed != true) {
try {
_package._plugin.busy {
_listeners?.invokeVoid("failure", exception.message);
_listeners?.invokeV8Void("failure", exception.message);
}
}
catch(ex: Throwable){
@@ -62,7 +62,7 @@ class DownloadService : Service() {
Logger.i(TAG, "onStartCommand");
synchronized(this) {
if(_started)
return START_STICKY;
return START_NOT_STICKY;
if(!FragmentedStorage.isInitialized) {
Logger.i(TAG, "Attempted to start DownloadService without initialized files");
@@ -78,7 +78,13 @@ class StateSync {
onAuthorized = { sess, isNewlyAuthorized, isNewSession ->
if (isNewSession) {
deviceUpdatedOrAdded.emit(sess.remotePublicKey, sess)
StateApp.instance.scope.launch(Dispatchers.IO) { checkForSync(sess) }
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
checkForSync(sess)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to check for sync.", e)
}
}
}
}
@@ -43,7 +43,6 @@ class HistoryListViewHolder : ViewHolder {
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) {
_root = itemView.findViewById(R.id.root);
_imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail);
_imageThumbnail.clipToOutline = true;
_textName = itemView.findViewById(R.id.text_video_name);
_textAuthor = itemView.findViewById(R.id.text_author);
_textMetadata = itemView.findViewById(R.id.text_video_metadata);
@@ -51,7 +51,6 @@ class VideoListEditorViewHolder : ViewHolder {
constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) {
_root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true;
_textName = view.findViewById(R.id.text_video_name);
_textAuthor = view.findViewById(R.id.text_author);
_textMetadata = view.findViewById(R.id.text_video_metadata);
@@ -29,7 +29,6 @@ class VideoListHorizontalViewHolder : ViewHolder {
constructor(view: View) : super(view) {
_root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true;
_textName = view.findViewById(R.id.text_video_name);
_textAuthor = view.findViewById(R.id.text_author);
_textVideoDuration = view.findViewById(R.id.thumbnail_duration);
@@ -1,5 +1,6 @@
package com.futo.platformplayer.views.livechat
import CSSColor
import android.graphics.Color
import android.graphics.drawable.LevelListDrawable
import android.text.Spannable
@@ -24,6 +25,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.overlays.LiveChatOverlay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import toAndroidColor
class LiveChatDonationListItem(viewGroup: ViewGroup)
: LiveChatListItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chat_donation, viewGroup, false)) {
@@ -55,10 +57,10 @@ class LiveChatDonationListItem(viewGroup: ViewGroup)
_amount.text = event.amount.trim();
if(event.colorDonation != null && event.colorDonation.isHexColor()) {
val color = Color.parseColor(event.colorDonation);
_amountContainer.background.setTint(color);
val color = CSSColor.parseColor(event.colorDonation);
_amountContainer.background.setTint(color.toAndroidColor());
if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400)
if(color.lightness > 0.5)
_amount.setTextColor(Color.BLACK);
else
_amount.setTextColor(Color.WHITE);
@@ -13,6 +13,7 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.live.LiveEventDonation
import com.futo.platformplayer.isHexColor
import toAndroidColor
class LiveChatDonationPill: LinearLayout {
private val _imageAuthor: ImageView;
@@ -33,10 +34,10 @@ class LiveChatDonationPill: LinearLayout {
if(donation.colorDonation != null && donation.colorDonation.isHexColor()) {
val color = Color.parseColor(donation.colorDonation);
root.background.setTint(color);
val color = CSSColor.parseColor(donation.colorDonation);
root.background.setTint(color.toAndroidColor());
if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400)
if(color.lightness > 0.5)
_textAmount.setTextColor(Color.BLACK);
else
_textAmount.setTextColor(Color.WHITE);
@@ -18,6 +18,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.overlays.LiveChatOverlay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import toAndroidColor
class LiveChatMessageListItem(viewGroup: ViewGroup)
: LiveChatListItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chat_message, viewGroup, false)) {
@@ -75,7 +76,7 @@ class LiveChatMessageListItem(viewGroup: ViewGroup)
if (!event.colorName.isNullOrEmpty()) {
try {
_authorName.setTextColor(Color.parseColor(event.colorName));
_authorName.setTextColor(CSSColor.parseColor(event.colorName).toAndroidColor());
} catch (ex: Throwable) {
}
} else
@@ -14,9 +14,6 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
@@ -43,6 +40,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import toAndroidColor
class LiveChatOverlay : LinearLayout {
@@ -66,10 +64,11 @@ class LiveChatOverlay : LinearLayout {
private val _overlayRaid: ConstraintLayout;
private val _overlayRaid_Name: TextView;
private val _overlayRaid_Message: TextView;
private val _overlayRaid_Thumbnail: ImageView;
private val _overlayRaid_ButtonGo: Button;
private val _overlayRaid_ButtonPrevent: Button;
private val _overlayRaid_ButtonDismiss: Button;
private val _textViewers: TextView;
@@ -148,9 +147,10 @@ class LiveChatOverlay : LinearLayout {
_overlayRaid = findViewById(R.id.overlay_raid);
_overlayRaid_Name = findViewById(R.id.raid_name);
_overlayRaid_Message = findViewById(R.id.textRaidMessage);
_overlayRaid_Thumbnail = findViewById(R.id.raid_thumbnail);
_overlayRaid_ButtonGo = findViewById(R.id.raid_button_go);
_overlayRaid_ButtonPrevent = findViewById(R.id.raid_button_prevent);
_overlayRaid_ButtonDismiss = findViewById(R.id.raid_button_prevent);
_overlayRaid.visibility = View.GONE;
@@ -159,7 +159,7 @@ class LiveChatOverlay : LinearLayout {
onRaidNow.emit(it);
}
}
_overlayRaid_ButtonPrevent.setOnClickListener {
_overlayRaid_ButtonDismiss.setOnClickListener {
_currentRaid?.let {
_currentRaid = null;
_overlayRaid.visibility = View.GONE;
@@ -291,10 +291,10 @@ class LiveChatOverlay : LinearLayout {
_overlayDonation_Amount.text = donation.amount.trim();
_overlayDonation.visibility = VISIBLE;
if(donation.colorDonation != null && donation.colorDonation.isHexColor()) {
val color = Color.parseColor(donation.colorDonation);
_overlayDonation_AmountContainer.background.setTint(color);
val color = CSSColor.parseColor(donation.colorDonation);
_overlayDonation_AmountContainer.background.setTint(color.toAndroidColor());
if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400)
if(color.lightness > 0.5)
_overlayDonation_Amount.setTextColor(Color.BLACK)
else
_overlayDonation_Amount.setTextColor(Color.WHITE);
@@ -372,6 +372,15 @@ class LiveChatOverlay : LinearLayout {
}
else
_overlayRaid.visibility = View.GONE;
if(raid?.isOutgoing ?: false) {
_overlayRaid_ButtonGo.visibility = View.VISIBLE
_overlayRaid_Message.text = "Viewers are raiding";
}
else {
_overlayRaid_ButtonGo.visibility = View.GONE;
_overlayRaid_Message.text = "Raid incoming from";
}
}
}
fun setViewCount(viewCount: Int) {
@@ -1,5 +1,6 @@
package com.futo.platformplayer.views.video
import android.animation.ValueAnimator
import android.content.Context
import android.content.Intent
import android.content.res.Resources
@@ -44,8 +45,11 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.behavior.GestureControlView
import com.futo.platformplayer.views.others.ProgressBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors
@@ -150,6 +154,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onChapterClicked = Event1<IChapter>();
private val loaderOverlay: FrameLayout
private val loaderIndeterminate: android.widget.ProgressBar
private val loaderDeterminate: android.widget.ProgressBar
private var determinateAnimator: ValueAnimator? = null
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
LayoutInflater.from(context).inflate(R.layout.video_view, this, true);
@@ -190,6 +199,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration);
_control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause);
loaderOverlay = findViewById(R.id.loader_overlay)
loaderIndeterminate = findViewById(R.id.loader_indeterminate)
loaderDeterminate = findViewById(R.id.loader_determinate)
loaderOverlay.visibility = View.GONE
loaderIndeterminate.visibility = View.GONE
loaderDeterminate.visibility = View.GONE
_control_chapter.setOnClickListener {
_currentChapter?.let {
onChapterClicked.emit(it);
@@ -865,4 +882,39 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
override fun onSurfaceSizeChanged(width: Int, height: Int) {
gestureControl.resetZoomPan()
}
override fun setLoading(isLoading: Boolean) {
determinateAnimator?.cancel()
if (isLoading) {
loaderOverlay.visibility = View.VISIBLE
loaderIndeterminate.visibility = View.VISIBLE
loaderDeterminate.visibility = View.GONE
} else {
loaderOverlay.visibility = View.GONE
loaderIndeterminate.visibility = View.GONE
loaderDeterminate.visibility = View.GONE
}
}
override fun setLoading(expectedDurationMs: Int) {
determinateAnimator?.cancel()
loaderOverlay.visibility = View.VISIBLE
loaderIndeterminate.visibility = View.GONE
loaderDeterminate.visibility = View.VISIBLE
loaderDeterminate.max = expectedDurationMs
loaderDeterminate.progress = 0
determinateAnimator = ValueAnimator.ofInt(0, expectedDurationMs).apply {
duration = expectedDurationMs.toLong()
addUpdateListener { anim ->
loaderDeterminate.progress = anim.animatedValue as Int
if(loaderDeterminate.progress > loaderDeterminate.max - 10) {
setLoading(true);
}
}
start()
};
}
}
@@ -65,6 +65,8 @@ import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
import getHttpDataSourceFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
@@ -567,11 +569,23 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
if(videoSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
val scope = this;
var startId = -1;
try {
val plugin = videoSource.getUnderlyingPlugin() ?: return@launch;
startId = plugin.getUnderlyingPlugin()?.runtimeId ?: -1;
val generated = plugin.busy { videoSource.generate(); };
val generatedDef = plugin.busy { videoSource.generateAsync(scope); };
withContext(Dispatchers.Main) {
if (generatedDef.estDuration >= 0) {
setLoading(generatedDef.estDuration)
} else {
setLoading(true)
}
}
val generated = generatedDef.await();
withContext(Dispatchers.Main) {
setLoading(false)
}
if (generated != null) {
withContext(Dispatchers.Main) {
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
@@ -608,6 +622,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
catch(ex: Throwable) {
Logger.e(TAG, "DashRaw generator failed", ex);
} finally {
withContext(Dispatchers.Main) {
setLoading(false)
}
}
}
return false;
@@ -694,10 +712,23 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
Logger.i(TAG, "Loading AudioSource [DashRaw]");
if(audioSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
val scope = this;
var startId = -1;
try {
val plugin = audioSource.getUnderlyingPlugin() ?: return@launch;
startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1;
val generated = audioSource.generate();
val generatedDef = plugin.busy { audioSource.generateAsync(scope); }
withContext(Dispatchers.Main) {
if (generatedDef.estDuration >= 0) {
setLoading(generatedDef.estDuration)
} else {
setLoading(true)
}
}
val generated = generatedDef.await();
withContext(Dispatchers.Main) {
setLoading(false)
}
if(generated != null) {
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
audioSource.getHttpDataSourceFactory()
@@ -724,6 +755,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
catch(ex: Throwable) {
} finally {
withContext(Dispatchers.Main) {
setLoading(false)
}
}
}
return false;
@@ -933,6 +968,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
}
protected open fun setLoading(isLoading: Boolean) { }
protected open fun setLoading(expectedDurationMs: Int) { }
companion object {
val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#ff5a5d5a"
android:centerY="0.75"
android:endColor="#ff5a5d5a"
android:startColor="#ff5a5d5a" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#80ffb600"
android:centerY="0.75"
android:endColor="#80ffd300"
android:startColor="#80ffd300" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:endColor="#3333FF"
android:startColor="#3333FF" />
</shape>
</clip>
</item>
</layer-list>
@@ -76,4 +76,15 @@
app:buttonIcon="@drawable/ic_copy"
android:layout_marginTop="8dp" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_loader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+5 -5
View File
@@ -263,8 +263,8 @@
android:textSize="13dp"
android:letterSpacing="0"
android:fontFamily="@font/inter_regular"
android:layout_marginStart="20dp"
android:backgroundTint="#2F2F2F"
android:layout_marginStart="5dp"
android:backgroundTint="@color/colorPrimary"
android:layout_marginEnd="5dp"
android:text="@string/go_now"/>
<Button
@@ -277,9 +277,9 @@
android:textSize="13dp"
android:letterSpacing="0"
android:layout_marginStart="5dp"
android:layout_marginEnd="20dp"
android:backgroundTint="#481414"
android:text="@string/prevent"/>
android:layout_marginEnd="5dp"
android:backgroundTint="#2F2F2F"
android:text="@string/dismiss"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
+33
View File
@@ -64,4 +64,37 @@
app:controller_layout_id="@layout/video_player_ui_fullscreen"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/loader_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="6dp"
android:background="@color/black"
android:clickable="true"
android:focusable="true"
android:visibility="gone">
<ProgressBar
android:id="@+id/loader_indeterminate"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
<ProgressBar
android:id="@+id/loader_determinate"
style="@android:style/Widget.ProgressBar.Horizontal"
android:progressDrawable="@drawable/progress_bar"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="false"
android:max="100"
android:progress="0"
android:visibility="gone"/>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,257 @@
package com.futo.platformplayer
import CSSColor
import org.junit.Assert.assertEquals
import kotlin.math.PI
import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertTrue
class CSSColorTest {
private fun approxEq(expected: Float, actual: Float, eps: Float = 1e-5f) {
assertTrue(abs(expected - actual) <= eps, "Expected $expected but got $actual")
}
@Test fun `hex #RRGGBB parses correctly`() {
val c = CSSColor.parseColor("#336699")
assertEquals(0x33, c.red)
assertEquals(0x66, c.green)
assertEquals(0x99, c.blue)
assertEquals(255, c.alpha)
}
@Test fun `hex #RGB shorthand expands`() {
val c = CSSColor.parseColor("#369")
assertEquals(0x33, c.red)
assertEquals(0x66, c.green)
assertEquals(0x99, c.blue)
}
@Test fun `hex #RRGGBBAA parses alpha`() {
val c = CSSColor.parseColor("#33669980")
assertEquals(0x33, c.red)
assertEquals(0x66, c.green)
assertEquals(0x99, c.blue)
approxEq(128 / 255f, c.a)
assertEquals(128, c.alpha)
}
@Test fun `hex #RGBA shorthand expands with alpha`() {
val c = CSSColor.parseColor("#3698")
assertEquals(0x33, c.red)
assertEquals(0x66, c.green)
assertEquals(0x99, c.blue)
assertEquals(0x88, c.alpha)
}
@Test fun `hex uppercase shorthand parses`() {
val c = CSSColor.parseColor("#AbC")
// expands to AABBCC
assertEquals(0xAA, c.red)
assertEquals(0xBB, c.green)
assertEquals(0xCC, c.blue)
}
@Test fun `rgb(ints) functional parser`() {
val c = CSSColor.parseColor("rgb(255,128,0)")
assertEquals(255, c.red)
assertEquals(128, c.green)
assertEquals(0, c.blue)
assertEquals(255, c.alpha)
}
@Test fun `rgb(percent) functional parser`() {
val c = CSSColor.parseColor("rgb(100%,50%,0%)")
assertEquals(255, c.red)
assertEquals(128, c.green)
assertEquals(0, c.blue)
}
@Test fun `rgba rawfloat alpha functional parser`() {
val c = CSSColor.parseColor("rgba(255,0,0,0.5)")
assertEquals(255, c.red)
assertEquals(0, c.green)
assertEquals(0, c.blue)
approxEq(0.5f, c.a)
}
@Test fun `rgba percent alpha functional parser`() {
val c = CSSColor.parseColor("rgba(100%,0%,0%,50%)")
assertEquals(255, c.red)
assertEquals(0, c.green)
assertEquals(0, c.blue)
approxEq(0.5f, c.a)
}
@Test fun `hsl() functional parser yields correct RGB`() {
// pure green: hue=120°, sat=100%, light=50%
val c = CSSColor.parseColor("hsl(120,100%,50%)")
assertEquals(0, c.red)
assertEquals(255, c.green)
assertEquals(0, c.blue)
}
@Test fun `hsla percent alpha functional parser`() {
val c = CSSColor.parseColor("hsla(240,100%,50%,25%)")
// pure blue, alpha 25%
assertEquals(0, c.red)
assertEquals(0, c.green)
assertEquals(255, c.blue)
approxEq(0.25f, c.a)
}
@Test fun `hsla rawfloat alpha functional parser`() {
val c = CSSColor.parseColor("hsla(240,100%,50%,0.25)")
assertEquals(0, c.red)
assertEquals(0, c.green)
assertEquals(255, c.blue)
approxEq(0.25f, c.a)
}
@Test fun `hsl radian unit parsing`() {
// 180° = π radians → cyan
val c = CSSColor.parseColor("hsl(${PI}rad,100%,50%)")
assertEquals(0, c.red)
assertEquals(255, c.green)
assertEquals(255, c.blue)
}
@Test fun `hsl turn unit parsing`() {
// 0.5 turn = 180° → cyan
val c = CSSColor.parseColor("hsl(0.5turn,100%,50%)")
assertEquals(0, c.red)
assertEquals(255, c.green)
assertEquals(255, c.blue)
}
@Test fun `hsl grad unit parsing`() {
// 200 grad = 180° → cyan
val c = CSSColor.parseColor("hsl(200grad,100%,50%)")
assertEquals(0, c.red)
assertEquals(255, c.green)
assertEquals(255, c.blue)
}
@Test fun `named colors parse`() {
val red = CSSColor.parseColor("red")
assertEquals(255, red.red)
assertEquals(0, red.green)
assertEquals(0, red.blue)
val rebecca = CSSColor.parseColor("rebeccapurple")
assertEquals(0x66, rebecca.red)
assertEquals(0x33, rebecca.green)
assertEquals(0x99, rebecca.blue)
val transparent = CSSColor.parseColor("transparent")
assertEquals(0, transparent.alpha)
}
@Test fun `round-trip Android Int ↔ CSSColor`() {
val original = CSSColor(0.2f, 0.4f, 0.6f, 0.8f)
val colorInt = original.toRgbaInt()
val back = CSSColor.fromRgba(colorInt)
approxEq(original.r, back.r)
approxEq(original.g, back.g)
approxEq(original.b, back.b)
approxEq(original.a, back.a)
}
@Test fun `individual channel setters`() {
val c = CSSColor(0f,0f,0f,1f)
c.red = 128; assertEquals(128, c.red); approxEq(128/255f, c.r)
c.green = 64; assertEquals(64, c.green); approxEq(64/255f, c.g)
c.blue = 32; assertEquals(32, c.blue); approxEq(32/255f, c.b)
c.alpha = 200; assertEquals(200, c.alpha); approxEq(200/255f, c.a)
}
@Test fun `hsl channel setters update RGB`() {
val c = CSSColor.parseColor("hsl(0,100%,50%)") // red
c.hue = 120f // → green
assertEquals(0, c.red)
assertEquals(255, c.green)
assertEquals(0, c.blue)
c.saturation = 0f // → gray
assertTrue(c.red == c.green && c.green == c.blue)
}
@Test fun `convenience modifiers chain as expected`() {
val c = CSSColor.parseColor("#888888")
.lighten(0.1f)
.saturate(0.2f)
.rotateHue(45f)
approxEq(0.633f, c.lightness, eps = 1e-3f)
approxEq(0.2f, c.saturation, eps = 1e-3f)
approxEq(45f, c.hue)
}
@Test
fun `invalid formats throw IllegalArgumentException`() {
listOf("", "rgb()", "hsl(0,0)", "#12", "rgba(0,0,0,150%)", "hsla(0,0%,0%,2)").forEach { bad ->
try {
CSSColor.parseColor(bad)
assert(false)
} catch (e: Throwable) {
}
}
}
@Test fun `outofrange RGB ints clamp`() {
val c = CSSColor.parseColor("rgb(300,-20, 260)")
assertEquals(255, c.red)
assertEquals(0, c.green)
assertEquals(255, c.blue)
}
@Test fun `parser is case- and whitespace-tolerant`() {
val a = CSSColor.parseColor(" RgB( 10 ,20, 30 )")
assertEquals(10, a.red)
assertEquals(20, a.green)
assertEquals(30, a.blue)
val b = CSSColor.parseColor(" ReBeCcaPURple ")
assertEquals(0x66, b.red)
assertEquals(0x33, b.green)
assertEquals(0x99, b.blue)
}
@Test fun `hsl lightness extremes`() {
// lightness = 0 → black
val black = CSSColor.parseColor("hsl(123,45%,0%)")
assertEquals(0, black.red)
assertEquals(0, black.green)
assertEquals(0, black.blue)
// lightness = 100% → white
val white = CSSColor.parseColor("hsl(321,55%,100%)")
assertEquals(255, white.red)
assertEquals(255, white.green)
assertEquals(255, white.blue)
// saturation = 0 → gray (r==g==b)
val gray = CSSColor.parseColor("hsl(50,0%,60%)")
assertTrue(gray.red == gray.green && gray.green == gray.blue)
}
@Test fun `hsl negative and large hues wrap`() {
val c1 = CSSColor.parseColor("hsl(-120,100%,50%)") // → same as 240°
assertEquals(0, c1.red)
assertEquals(0, c1.green)
assertEquals(255, c1.blue)
val c2 = CSSColor.parseColor("hsl(480,100%,50%)") // → same as 120°
assertEquals(0, c2.red)
assertEquals(255, c2.green)
assertEquals(0, c2.blue)
}
@Test fun `lighten then darken returns original`() {
val base = CSSColor.parseColor("#123456")
val round = base.lighten(0.2f).darken(0.2f)
approxEq(base.r, round.r)
approxEq(base.g, round.g)
approxEq(base.b, round.b)
}
}
@@ -26,7 +26,7 @@ import java.util.Random
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/*
class NoiseProtocolTest {
constructor() {
Logger.setLogConsumers(listOf(
@@ -625,4 +625,4 @@ class NoiseProtocolTest {
throw Exception("Byte mismatch at index ${i}")
}
}
}
}*/