Compare commits

...

94 Commits

Author SHA1 Message Date
Kelvin 01cb544dfd Dont lock clients when disabling 2025-07-07 16:42:37 +02:00
Kelvin b9239b6177 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 16:18:35 +02:00
Kelvin 96ca3f62a2 Missing invokev8 wrappers 2025-07-07 16:18:23 +02:00
Koen J 73ad783881 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 16:12:33 +02:00
Koen J 3bfcf65535 Fixes to reload required exception handling for casting. 2025-07-07 16:12:03 +02:00
Kelvin 8b3b27a2a8 Stop pagers silently if the underlying object is closed 2025-07-07 16:04:19 +02:00
Kelvin a4d4835a89 Reduice font size 2025-07-07 14:26:17 +02:00
Kelvin 56c0f7bfaf Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-07-07 14:14:51 +02:00
Kelvin 736424ae35 Refs 2025-07-07 14:14:39 +02:00
Koen J 37dc778009 Fixed casting. 2025-07-07 12:45:45 +02:00
Koen J cd3cea58a4 Fixed race condition when awaiting and changing video source.. 2025-07-07 10:52:42 +02:00
Koen J 8b53e9e5e3 Processed last feedback on minigame. 2025-07-05 18:09:49 +02:00
Koen J 08e98b089c Improvements to target tap loader game. 2025-07-05 17:32:31 +02:00
Koen J 5528d71da8 Show score toast. 2025-07-05 14:07:49 +02:00
Koen J 83f520ca44 Further fixes to TargetTApLoaderView. 2025-07-05 13:47:48 +02:00
Koen J cc247ce634 Attempt at a loader game. 2025-07-05 12:58:33 +02:00
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
Kelvin K 44ff951ec6 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 16:22:51 +02:00
Kelvin K 11319e0ec5 Refs 2025-06-18 16:22:28 +02:00
Koen J 100e98a960 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 15:43:45 +02:00
Koen J c6100ede70 Added disable for hold playback rate increase. 2025-06-18 15:43:12 +02:00
Kelvin K a2986a72bd Refs 2025-06-18 14:43:20 +02:00
Kelvin K e0e90c5f74 submodules 2025-06-18 14:33:07 +02:00
Kelvin K 11992af81b Hide duration if unknown 2025-06-18 14:27:20 +02:00
Koen J 15d771f7fc Fixed channel loader not being animated. 2025-06-18 13:43:50 +02:00
Kelvin K 5ede474253 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 12:41:43 +02:00
Kelvin K 7922aa6f80 Log on busy on main 2025-06-18 12:41:21 +02:00
Kelvin K 0c1333fa15 Downgrade v8, revert comments on diff thread 2025-06-18 12:40:25 +02:00
Koen J 53b9ba0368 Reverted changes. 2025-06-18 10:29:12 +02:00
Koen J c3a8877796 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-18 10:07:29 +02:00
Koen J a464ae9df5 Added missing loader causing crash. 2025-06-18 10:07:02 +02:00
Kelvin K 0d16dd0006 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-06-17 17:28:17 +02:00
Kelvin K 48a96140a7 isBusy checks and locking improvements 2025-06-17 17:28:10 +02:00
Kelvin 603ef8f295 Merge branch 'fix-timeout-locking' into 'master'
fix: timeoutMap being deadlocked

See merge request videostreaming/grayjay!126
2025-06-17 15:26:51 +00:00
zvonimir ab07288ba0 fix: timeoutMap being deadlocked 2025-06-17 17:25:34 +02:00
Kelvin K c0bbe5d491 Additional locking 2025-06-17 15:21:46 +02:00
Kelvin K b953ff21e7 Lock on subtitle fetch 2025-06-17 11:52:26 +02:00
Kelvin K c14378b534 Improved V8 locking, comment section on diff thread than video, global mapping of v8runtimes to plugins 2025-06-17 11:45:02 +02:00
Kelvin K 33d3d9a29c Improved locking 2025-06-16 19:30:52 +02:00
Kelvin K 7e83793586 Submods 2025-06-16 18:34:37 +02:00
Kelvin K 6ba9ec8bc2 Clearer name setting 2025-06-16 17:56:04 +02:00
Kelvin 0b02ab0e2d Merge branch 'plugin-fixes' into 'master'
V8 Update, V8 interaction locking, Package fixes, ReloadRequiredException support

See merge request videostreaming/grayjay!125
2025-06-16 15:48:01 +00:00
Kelvin K ff531b5e77 Cleanup, fixes, clearCookies support on httpClients 2025-06-16 17:46:00 +02:00
Kelvin K b3f9de3b83 edgecase fix 2025-06-16 14:23:34 +02:00
Kelvin K 86bd71b89c Fix edgecase 2025-06-16 14:19:23 +02:00
Kelvin K 2fca7e9a01 Locking of most known v8 interactions, fix returning previously returned jvm objects, Related fixes 2025-06-16 14:13:47 +02:00
Koen 2cc873ef60 Merge branch 'quality-selector-fix' into 'master'
fix graphical glitches with quality selector

See merge request videostreaming/grayjay!109
2025-06-16 10:07:16 +00:00
Koen 7a66ce6bcd Merge branch 'sources-tab-scrolling-fix' into 'master'
Sources Scrolling Fix

See merge request videostreaming/grayjay!114
2025-06-16 10:01:45 +00:00
Koen 2730569b6b Merge branch 'tablet-landscape-fix' into 'master'
Tablet Landscape Fix

See merge request videostreaming/grayjay!115
2025-06-16 09:57:57 +00:00
Koen ede5c4409c Merge branch 'watch-later-add-feature' into 'master'
Water Later Add Feature

See merge request videostreaming/grayjay!117
2025-06-16 09:54:43 +00:00
Koen 0dbe398435 Merge branch 'hls-quality-sort' into 'master'
Adaptive Quality Sort

See merge request videostreaming/grayjay!118
2025-06-16 09:41:22 +00:00
Koen J bcab3bccbc Fixed crash when signature fields are wrongly populated. 2025-06-16 10:43:57 +02:00
Kelvin K 58c9aeb1a2 WIP: V8 update, package http fixes, ReloadRequiredException support, other fixes. Currently broken in situations where setTimeout is used 2025-06-14 15:51:31 +02:00
Kelvin K 4702787784 WIP 2025-06-13 17:47:22 +02:00
Koen J 13100dc38d Minor fix in playback speed setting. 2025-06-12 11:21:00 +02:00
Koen J 5227041398 Added setting for hold playback speed increase. Implemented chromecast playback rate adjustment in range [1, 2]. Implemented hold playback speed increase pill. 2025-06-12 10:33:05 +02:00
Kelvin 8491d4da1a Merge branch 'fix-ump-downloads' into 'master'
Revert downloads patch which broke downloads

See merge request videostreaming/grayjay!122
2025-06-11 16:41:20 +00:00
zvonimir 9bea1563ca Revert downloads patch which broke downloads 2025-06-11 18:36:05 +02:00
Koen J 9e7b936663 Implemented hold to play video at 2x speed gesture. 2025-06-11 17:03:53 +02:00
Kelvin 19c84475db Hotfix playback speed for non-dot locales 2025-06-10 23:27:01 +02:00
Kai 9944842a2f Change adaptive streaming (HLS and Dash) quality to sort in descending quality to align with YouTube and the rest of Grayjay
Changelog: changed
2025-06-09 17:02:55 -05:00
Kai 99dc50894c update text
Changelog: changed
2025-06-09 16:54:24 -05:00
Kai 6598dff6df add add to watch later setting
add https://github.com/futo-org/grayjay-android/issues/2173

Changelog: added
2025-06-06 23:35:59 -05:00
Kai 623c47fa2e fix https://github.com/futo-org/grayjay-android/issues/2210
Changelog: changed
2025-06-06 15:25:46 -05:00
Kai 19861fe812 fix https://github.com/futo-org/grayjay-android/issues/2316
Changelog: changed
2025-06-06 13:40:20 -05:00
Kai c333300906 fix graphical glitches with quality selector
Changelog: changed
2025-06-05 11:08:19 -05:00
133 changed files with 3012 additions and 611 deletions
+1
View File
@@ -180,6 +180,7 @@ dependencies {
//JS //JS
implementation("com.caoccao.javet:javet-android:3.0.2") implementation("com.caoccao.javet:javet-android:3.0.2")
//implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
//Exoplayer //Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1' implementation 'androidx.media3:media3-exoplayer:1.2.1'
@@ -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()
)
}
}
@@ -11,7 +11,7 @@ import java.nio.ByteBuffer
import kotlin.random.Random import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
/*
class SyncServerTests { class SyncServerTests {
//private val relayHost = "relay.grayjay.app" //private val relayHost = "relay.grayjay.app"
@@ -335,4 +335,4 @@ class SyncServerTests {
class AlwaysAuthorized : IAuthorizable { class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true override val isAuthorized: Boolean get() = true
} }*/
@@ -13,7 +13,7 @@ import kotlin.random.Random
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
/*
data class PipeStreams( data class PipeStreams(
val initiatorInput: LittleEndianDataInputStream, val initiatorInput: LittleEndianDataInputStream,
val initiatorOutput: LittleEndianDataOutputStream, val initiatorOutput: LittleEndianDataOutputStream,
@@ -509,4 +509,4 @@ class Authorized : IAuthorizable {
class Unauthorized : IAuthorizable { class Unauthorized : IAuthorizable {
override val isAuthorized: Boolean = false override val isAuthorized: Boolean = false
} }*/
+8 -1
View File
@@ -103,6 +103,12 @@ class UnavailableException extends ScriptException {
super("UnavailableException", msg); super("UnavailableException", msg);
} }
} }
class ReloadRequiredException extends ScriptException {
constructor(msg, reloadData) {
super("ReloadRequiredException", msg);
this.reloadData = reloadData;
}
}
class AgeException extends ScriptException { class AgeException extends ScriptException {
constructor(msg) { constructor(msg) {
super("AgeException", msg); super("AgeException", msg);
@@ -701,11 +707,12 @@ class LiveEventViewCount extends LiveEvent {
} }
} }
class LiveEventRaid extends LiveEvent { class LiveEventRaid extends LiveEvent {
constructor(targetUrl, targetName, targetThumbnail) { constructor(targetUrl, targetName, targetThumbnail, isOutgoing) {
super(100); super(100);
this.targetUrl = targetUrl; this.targetUrl = targetUrl;
this.targetName = targetName; this.targetName = targetName;
this.targetThumbnail = targetThumbnail; 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,10 +2,30 @@ package com.futo.platformplayer
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.* 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.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject 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.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.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 //V8
@@ -24,6 +44,10 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
return handler(this); return handler(this);
} }
inline fun V8Value.getSourcePlugin(): V8Plugin? {
return V8Plugin.getPluginFromRuntime(this.v8Runtime);
}
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
if(this !is T) if(this !is T)
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}"); throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
@@ -89,7 +113,29 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
.map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T }; .map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T };
} }
inline fun V8Plugin.ensureIsBusy() {
this.let {
if (!it.isThreadAlreadyBusy()) {
//throw IllegalStateException("Tried to access V8Plugin without busy");
val stacktrace = Thread.currentThread().stackTrace;
Logger.w("Extensions_V8",
"V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
", " + stacktrace.drop(4)?.firstOrNull().toString() +
", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
", " + stacktrace.drop(6)?.firstOrNull()?.toString()
);
}
}
}
inline fun V8Value.ensureIsBusy() {
this?.getSourcePlugin()?.let {
it.ensureIsBusy();
}
}
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
if(false)
ensureIsBusy();
return when(T::class) { return when(T::class) {
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T; String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
Int::class -> { Int::class -> {
@@ -146,4 +192,137 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() }) for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
map.put(prop, obj.getString(prop)); map.put(prop, obj.getString(prop));
return map; 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));
} }
@@ -584,6 +584,25 @@ class Settings : FragmentedStorageFileJson() {
playbackSpeeds.sort(); playbackSpeeds.sort();
return playbackSpeeds; return playbackSpeeds;
} }
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
var holdPlaybackSpeed: Int = 4;
fun getHoldPlaybackSpeed(): Double {
return when(holdPlaybackSpeed) {
0 -> 1.0
1 -> 1.25
2 -> 1.5
3 -> 1.75
4 -> 2.0
5 -> 2.25
6 -> 2.5
7 -> 2.75
8 -> 3.0
else -> 2.0
}
}
} }
@FormField(R.string.comments, "group", R.string.comments_description, 6) @FormField(R.string.comments, "group", R.string.comments_description, 6)
@@ -999,10 +1018,13 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true; var playlistAllowDups: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4) @FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
var watchLaterAddStart: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5) @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
var polycentricLocalCache: Boolean = true; var polycentricLocalCache: Boolean = true;
} }
@@ -1151,6 +1151,8 @@ class UISlideOverlays {
call = { call = {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false); UIDialogs.appToast("Added to watch later", false);
else
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
}), }),
) )
); );
@@ -32,7 +32,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStateAtLeast
import androidx.lifecycle.withStateAtLeast import androidx.lifecycle.withStateAtLeast
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.BuildConfig
@@ -114,7 +113,6 @@ import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.LinkedList import java.util.LinkedList
import java.util.Queue
import java.util.UUID import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@@ -610,6 +608,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}, UIDialogs.ActionStyle.PRIMARY) }, UIDialogs.ActionStyle.PRIMARY)
) )
} }
//startActivity(Intent(this, TestActivity::class.java))
} }
/* /*
@@ -14,10 +14,12 @@ import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.ContentType 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.BarcodeFormat
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix import com.google.zxing.common.BitMatrix
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import userpackage.Protocol.ExportBundle import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo import userpackage.Protocol.URLInfo
@@ -39,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _imageQR: ImageView; private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String; private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView; private lateinit var _textQR: TextView;
private lateinit var _loader: View
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -49,24 +55,47 @@ class PolycentricBackupActivity : AppCompatActivity() {
setContentView(R.layout.activity_polycentric_backup); setContentView(R.layout.activity_polycentric_backup);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
_buttonShare = findViewById(R.id.button_share); _buttonShare = findViewById(R.id.button_share)
_buttonCopy = findViewById(R.id.button_copy); _buttonCopy = findViewById(R.id.button_copy)
_imageQR = findViewById(R.id.image_qr); _imageQR = findViewById(R.id.image_qr)
_textQR = findViewById(R.id.text_qr); _textQR = findViewById(R.id.text_qr)
_loader = findViewById(R.id.progress_loader)
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
}; };
_exportBundle = createExportBundle(); _imageQR.visibility = View.INVISIBLE
_textQR.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
try { lifecycleScope.launch {
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt(); try {
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension); val pair = withContext(Dispatchers.IO) {
_imageQR.setImageBitmap(qrCodeBitmap); val bundle = createExportBundle()
} catch (e: Exception) { val dimension = TypedValue.applyDimension(
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e); TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
_imageQR.visibility = View.INVISIBLE; ).toInt()
_textQR.visibility = View.INVISIBLE; 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 { _buttonShare.onClick.subscribe {
@@ -2,12 +2,24 @@ package com.futo.platformplayer.activities
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.views.TargetTapLoaderView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class TestActivity : AppCompatActivity() { class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test); setContentView(R.layout.activity_test);
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
view.startLoader(10000)
lifecycleScope.launch {
delay(5000)
view.startLoader()
}
} }
companion object { companion object {
@@ -34,8 +34,10 @@ class PlatformClientPool {
isDead = true; isDead = true;
onDead.emit(parentClient, this); onDead.emit(parentClient, this);
for(clientPair in _pool) { synchronized(_pool) {
clientPair.key.disable(); for (clientPair in _pool) {
clientPair.key.disable();
}
} }
}; };
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
@@ -44,6 +45,7 @@ class PlatformID {
val NONE = PlatformID("Unknown", null); val NONE = PlatformID("Unknown", null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
value.ensureIsBusy();
val contextName = "PlatformID"; val contextName = "PlatformID";
return PlatformID( return PlatformID(
value.getOrThrow(config, "platform", contextName), value.getOrThrow(config, "platform", contextName),
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSContent import com.futo.platformplayer.api.media.platforms.js.models.JSContent
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -33,6 +34,7 @@ open class PlatformAuthorLink {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null); val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
value.ensureIsBusy();
if(value.has("membershipUrl")) if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value); return PlatformAuthorMembershipLink.fromV8(config, value);
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -20,6 +21,7 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
companion object { companion object {
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
value.ensureIsBusy();
val context = "AuthorMembershipLink" val context = "AuthorMembershipLink"
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)), return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
value.getOrThrow(config ,"name", context), value.getOrThrow(config ,"name", context),
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.expectV8Variant import com.futo.platformplayer.expectV8Variant
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -46,6 +47,7 @@ class ResultCapabilities(
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
val contextName = "ResultCapabilities"; val contextName = "ResultCapabilities";
value.ensureIsBusy();
return ResultCapabilities( return ResultCapabilities(
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") }, value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); }, value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
@@ -69,6 +71,7 @@ class FilterGroup(
companion object { companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
value.ensureIsBusy();
return FilterGroup( return FilterGroup(
value.getString("name"), value.getString("name"),
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null) value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
@@ -90,6 +93,7 @@ class FilterCapability(
companion object { companion object {
fun fromV8(obj: V8ValueObject): FilterCapability { fun fromV8(obj: V8ValueObject): FilterCapability {
obj.ensureIsBusy();
val value = obj.get("value") as V8Value; val value = obj.get("value") as V8Value;
return FilterCapability( return FilterCapability(
obj.getString("name"), obj.getString("name"),
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -31,6 +32,7 @@ class Thumbnails {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
value.ensureIsBusy();
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails")) return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray() .toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) } .map { Thumbnail.fromV8(config, it as V8ValueObject) }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
interface IPlatformLiveEvent { interface IPlatformLiveEvent {
@@ -10,6 +11,7 @@ interface IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
obj.ensureIsBusy();
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) { return when(t) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj); LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -27,6 +28,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
obj.ensureIsBusy();
val contextName = "LiveEventComment" val contextName = "LiveEventComment"
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null); val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -37,6 +38,7 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
obj.ensureIsBusy();
val contextName = "LiveEventDonation" val contextName = "LiveEventDonation"
return LiveEventDonation( return LiveEventDonation(
obj.getOrThrow(config, "name", contextName), obj.getOrThrow(config, "name", contextName),
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventEmojis: IPlatformLiveEvent { class LiveEventEmojis: IPlatformLiveEvent {
@@ -15,9 +16,9 @@ class LiveEventEmojis: IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
obj.ensureIsBusy();
val contextName = "LiveEventEmojis" val contextName = "LiveEventEmojis"
return LiveEventEmojis( return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
obj.getOrThrow(config, "emojis", contextName));
} }
} }
} }
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventRaid: IPlatformLiveEvent { class LiveEventRaid: IPlatformLiveEvent {
@@ -10,20 +12,24 @@ class LiveEventRaid: IPlatformLiveEvent {
val targetName: String; val targetName: String;
val targetThumbnail: String; val targetThumbnail: String;
val targetUrl: 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.targetName = name;
this.targetUrl = url; this.targetUrl = url;
this.targetThumbnail = thumbnail; this.targetThumbnail = thumbnail;
this.isOutgoing = isOutgoing;
} }
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
obj.ensureIsBusy();
val contextName = "LiveEventRaid" val contextName = "LiveEventRaid"
return LiveEventRaid( return LiveEventRaid(
obj.getOrThrow(config, "targetName", contextName), obj.getOrThrow(config, "targetName", contextName),
obj.getOrThrow(config, "targetUrl", contextName), obj.getOrThrow(config, "targetUrl", contextName),
obj.getOrThrow(config, "targetThumbnail", contextName)); obj.getOrThrow(config, "targetThumbnail", contextName),
obj.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
} }
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventViewCount: IPlatformLiveEvent { class LiveEventViewCount: IPlatformLiveEvent {
@@ -15,6 +16,7 @@ class LiveEventViewCount: IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
obj.ensureIsBusy();
val contextName = "LiveEventViewCount" val contextName = "LiveEventViewCount"
return LiveEventViewCount( return LiveEventViewCount(
obj.getOrThrow(config, "viewCount", contextName)); obj.getOrThrow(config, "viewCount", contextName));
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault import com.futo.platformplayer.orDefault
import com.futo.platformplayer.serializers.IRatingSerializer import com.futo.platformplayer.serializers.IRatingSerializer
@@ -13,8 +14,12 @@ interface IRating {
companion object { companion object {
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) }; fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating {
obj?.ensureIsBusy();
return obj.orDefault(default) { fromV8(config, it as V8ValueObject) }
};
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
obj.ensureIsBusy();
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) { return when(t) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj); RatingType.LIKES -> RatingLikes.fromV8(config, obj);
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@@ -14,6 +15,7 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
obj.ensureIsBusy();
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes")); return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@@ -13,6 +14,7 @@ class RatingLikes(val likes: Long) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
obj.ensureIsBusy();
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes")); return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@@ -13,6 +14,7 @@ class RatingScaler(val value: Float) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
obj.ensureIsBusy()
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler")); return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
} }
} }
@@ -56,6 +56,7 @@ class DevJSClient : JSClient {
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient { override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient {
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID); val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
client.setReloadData(getReloadData(true));
if (noSaveState) if (noSaveState)
client.initialize() client.initialize()
return client return client
@@ -59,9 +59,13 @@ import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.Random
import kotlin.Exception import kotlin.Exception
import kotlin.reflect.full.findAnnotations import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction import kotlin.reflect.jvm.kotlinFunction
@@ -83,6 +87,8 @@ open class JSClient : IPlatformClient {
private var _channelCapabilities: ResultCapabilities? = null; private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null; private var _peekChannelTypes: List<String>? = null;
private var _usedReloadData: String? = null;
protected val _script: String; protected val _script: String;
private var _initialized: Boolean = false; private var _initialized: Boolean = false;
@@ -98,14 +104,14 @@ open class JSClient : IPlatformClient {
override val icon: ImageVariable; override val icon: ImageVariable;
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities(); override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
private val _busyLock = Object();
private var _busyCounter = 0;
private var _busyAction = ""; private var _busyAction = "";
val isBusy: Boolean get() = _busyCounter > 0; val isBusy: Boolean get() = _plugin.isBusy;
val isBusyAction: String get() { val isBusyAction: String get() {
return _busyAction; return _busyAction;
} }
val declareOnEnable = HashMap<String, String>();
val settings: HashMap<String, String?> get() = descriptor.settings; val settings: HashMap<String, String?> get() = descriptor.settings;
val flags: Array<String>; val flags: Array<String>;
@@ -197,6 +203,7 @@ open class JSClient : IPlatformClient {
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient { open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials); val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
client.setReloadData(getReloadData(true));
if (noSaveState) if (noSaveState)
client.initialize() client.initialize()
return client return client
@@ -213,14 +220,31 @@ open class JSClient : IPlatformClient {
return plugin.httpClientOthers[id]; return plugin.httpClientOthers[id];
} }
fun setReloadData(data: String?) {
if(data == null) {
if(declareOnEnable.containsKey("__reloadData"))
declareOnEnable.remove("__reloadData");
}
else
declareOnEnable.put("__reloadData", data ?: "");
}
fun getReloadData(orLast: Boolean): String? {
if(declareOnEnable.containsKey("__reloadData"))
return declareOnEnable["__reloadData"];
else if(orLast)
return _usedReloadData;
return null;
}
override fun initialize() { override fun initialize() {
if (_initialized) return if (_initialized) return
Logger.i(TAG, "Plugin [${config.name}] initializing");
plugin.start(); plugin.start();
plugin.execute("plugin.config = ${Json.encodeToString(config)}"); plugin.execute("plugin.config = ${Json.encodeToString(config)}");
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})"); plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
descriptor.appSettings.loadDefaults(descriptor.config); descriptor.appSettings.loadDefaults(descriptor.config);
_initialized = true; _initialized = true;
@@ -260,19 +284,28 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started") @JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
fun enable() { fun enable() = isBusyWith("enable") {
if(!_initialized) if(!_initialized)
initialize(); initialize();
for(toDeclare in declareOnEnable) {
plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value));
}
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})"); plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
if(declareOnEnable.containsKey("__reloadData")) {
Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}");
_usedReloadData = declareOnEnable["__reloadData"];
declareOnEnable.remove("__reloadData");
}
_enabled = true; _enabled = true;
} }
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances") @JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
fun saveState(): String? { fun saveState(): String? = isBusyWith("saveState") {
ensureEnabled(); ensureEnabled();
if(!capabilities.hasSaveState) if(!capabilities.hasSaveState)
return null; return@isBusyWith null;
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value; val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
return resp; return@isBusyWith resp;
} }
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped") @JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
@@ -313,8 +346,10 @@ open class JSClient : IPlatformClient {
return _searchCapabilities!!; return _searchCapabilities!!;
} }
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()")); return busy {
return _searchCapabilities!!; _searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
return@busy _searchCapabilities!!;
}
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getSearchCapabilities", ex); announcePluginUnhandledException("getSearchCapabilities", ex);
@@ -342,8 +377,10 @@ open class JSClient : IPlatformClient {
if (_searchChannelContentsCapabilities != null) if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!; return _searchChannelContentsCapabilities!!;
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()")); return busy {
return _searchChannelContentsCapabilities!!; _searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
return@busy _searchChannelContentsCapabilities!!;
}
} }
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform") @JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
@JSDocsParameter("channelUrl", "Channel url to search") @JSDocsParameter("channelUrl", "Channel url to search")
@@ -375,14 +412,14 @@ open class JSClient : IPlatformClient {
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform") @JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
@JSDocsParameter("url", "A channel url (May not be your platform)") @JSDocsParameter("url", "A channel url (May not be your platform)")
override fun isChannelUrl(url: String): Boolean { override fun isChannelUrl(url: String): Boolean = isBusyWith("isChannelUrl") {
try { try {
return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})") return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
.value; .value;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex); announcePluginUnhandledException("isChannelUrl", ex);
return false; return@isBusyWith false;
} }
} }
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url") @JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@@ -400,9 +437,10 @@ open class JSClient : IPlatformClient {
if (_channelCapabilities != null) { if (_channelCapabilities != null) {
return _channelCapabilities!!; return _channelCapabilities!!;
} }
return busy {
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()")); _channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return _channelCapabilities!!; return@busy _channelCapabilities!!;
};
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getChannelCapabilities", ex); announcePluginUnhandledException("getChannelCapabilities", ex);
@@ -513,14 +551,14 @@ open class JSClient : IPlatformClient {
@JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform") @JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
@JSDocsParameter("url", "A content url (May not be your platform)") @JSDocsParameter("url", "A content url (May not be your platform)")
override fun isContentDetailsUrl(url: String): Boolean { override fun isContentDetailsUrl(url: String): Boolean = isBusyWith("isContentDetailsUrl") {
try { try {
return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})") return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
.value; .value;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isContentDetailsUrl", ex); announcePluginUnhandledException("isContentDetailsUrl", ex);
return false; return@isBusyWith false;
} }
} }
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url") @JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@@ -552,7 +590,7 @@ open class JSClient : IPlatformClient {
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})"); Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})"); val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
if(tracker is V8ValueObject) if(tracker is V8ValueObject)
return@isBusyWith JSPlaybackTracker(config, tracker); return@isBusyWith JSPlaybackTracker(this, tracker);
else else
return@isBusyWith null; return@isBusyWith null;
} }
@@ -594,7 +632,6 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})")); plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
} }
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page") @JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content") @JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") { override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
@@ -622,17 +659,19 @@ open class JSClient : IPlatformClient {
@JSOptional @JSOptional
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform") @JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist") @JSDocsParameter("url", "Url of playlist")
override fun isPlaylistUrl(url: String): Boolean { override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") {
if (!capabilities.hasGetPlaylist) if (!capabilities.hasGetPlaylist)
return false; return@isBusyWith false;
try { try {
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})") return@isBusyWith busy {
.value; return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex); announcePluginUnhandledException("isPlaylistUrl", ex);
return false; return@isBusyWith false;
} }
} }
@JSOptional @JSOptional
@@ -734,19 +773,29 @@ open class JSClient : IPlatformClient {
return urls; return urls;
} }
fun <T> busy(handle: ()->T): T {
private fun <T> isBusyWith(actionName: String, handle: ()->T): T { return _plugin.busy {
try { return@busy handle();
synchronized(_busyLock) {
_busyCounter++;
}
_busyAction = actionName;
return handle();
} }
finally { }
_busyAction = ""; fun <T> busyBlockingSuspended(handle: suspend ()->T): T {
synchronized(_busyLock) { return _plugin.busy {
_busyCounter--; return@busy runBlocking {
return@runBlocking handle();
}
}
}
fun <T> isBusyWith(actionName: String, handle: ()->T): T {
//val busyId = kotlin.random.Random.nextInt(9999);
return busy {
try {
_busyAction = actionName;
return@busy handle();
}
finally {
_busyAction = "";
} }
} }
} }
@@ -4,6 +4,7 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
@@ -168,12 +169,17 @@ class SourcePluginConfig(
} }
fun validate(text: String): Boolean { fun validate(text: String): Boolean {
if(scriptPublicKey.isNullOrEmpty()) try {
throw IllegalStateException("No public key present"); if (scriptPublicKey.isNullOrEmpty())
if(scriptSignature.isNullOrEmpty()) throw IllegalStateException("No public key present");
throw IllegalStateException("No signature present"); if (scriptSignature.isNullOrEmpty())
throw IllegalStateException("No signature present");
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey); return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
return false
}
} }
fun isUrlAllowed(url: String): Boolean { fun isUrlAllowed(url: String): Boolean {
@@ -204,6 +210,8 @@ class SourcePluginConfig(
obj.sourceUrl = sourceUrl; obj.sourceUrl = sourceUrl;
return obj; return obj;
} }
private val TAG = "SourcePluginConfig"
} }
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@@ -67,6 +67,25 @@ class JSHttpClient : ManagedHttpClient {
} }
fun resetAuthCookies() {
_currentCookieMap.clear();
if(!_auth?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _auth!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
if(!_captcha?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _captcha!!.cookieMap!!) {
if(_currentCookieMap.containsKey(domainCookies.key))
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
else
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
}
}
fun clearOtherCookies() {
_otherCookieMap.clear();
}
override fun clone(): ManagedHttpClient { override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth); val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -13,6 +14,7 @@ interface IJSContent: IPlatformContent {
companion object { companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
obj.ensureIsBusy();
val config = plugin.config; val config = plugin.config;
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem"); val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null); val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
@@ -6,12 +6,14 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
interface IJSContentDetails: IPlatformContent { interface IJSContentDetails: IPlatformContent {
companion object { companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
obj.ensureIsBusy();
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails"); val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
return when(ContentType.fromInt(type)) { return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj); ContentType.MEDIA -> JSVideoDetails(plugin, obj);
@@ -21,6 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails { open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
@@ -85,12 +86,12 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager); return JSContentPager(_pluginConfig, client, contentPager);
} }
private fun getCommentsJS(client: JSClient): JSCommentPager { private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>()); val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager); return JSCommentPager(_pluginConfig, client, commentPager);
} }
@@ -12,6 +12,7 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -60,7 +61,7 @@ class JSComment : IPlatformComment {
if(!_hasGetReplies) if(!_hasGetReplies)
return null; return null;
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>()); val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient"); val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return JSCommentPager(_config!!, plugin, obj); return JSCommentPager(_config!!, plugin, obj);
} }
@@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }
override fun nextPage() { override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") {
super.nextPage(); super.nextPage();
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
abstract class JSPager<T> : IPager<T> { abstract class JSPager<T> : IPager<T> {
@@ -29,7 +30,9 @@ abstract class JSPager<T> : IPager<T> {
this.pager = pager; this.pager = pager;
this.config = config; this.config = config;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; plugin.busy {
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}
getResults(); getResults();
} }
@@ -38,17 +41,20 @@ abstract class JSPager<T> : IPager<T> {
} }
override fun hasMorePages(): Boolean { override fun hasMorePages(): Boolean {
return _hasMorePages; return _hasMorePages && !pager.isClosed;
} }
override fun nextPage() { override fun nextPage() {
warnIfMainThread("JSPager.nextPage"); warnIfMainThread("JSPager.nextPage");
pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { val pluginV8 = plugin.getUnderlyingPlugin();
pager.invoke("nextPage", arrayOf<Any>()); pluginV8.busy {
}; pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; pager.invokeV8("nextPage", arrayOf<Any>());
_resultChanged = true; };
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
}
/* /*
try { try {
} }
@@ -70,15 +76,18 @@ abstract class JSPager<T> : IPager<T> {
return previousResults; return previousResults;
warnIfMainThread("JSPager.getResults"); warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if(items.v8Runtime.isDead || items.v8Runtime.isClosed) return plugin.getUnderlyingPlugin().busy {
throw IllegalStateException("Runtime closed"); val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
val newResults = items.toArray() if (items.v8Runtime.isDead || items.v8Runtime.isClosed)
.map { convertResult(it as V8ValueObject) } throw IllegalStateException("Runtime closed");
.toList(); val newResults = items.toArray()
_lastResults = newResults; .map { convertResult(it as V8ValueObject) }
_resultChanged = false; .toList();
return newResults; _lastResults = newResults;
_resultChanged = false;
return@busy newResults;
}
} }
abstract fun convertResult(obj: V8ValueObject): T; abstract fun convertResult(obj: V8ValueObject): T;
@@ -2,37 +2,51 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
class JSPlaybackTracker: IPlaybackTracker { class JSPlaybackTracker: IPlaybackTracker {
private val _config: IV8PluginConfig; private lateinit var _client: JSClient;
private val _obj: V8ValueObject; private lateinit var _config: IV8PluginConfig;
private lateinit var _obj: V8ValueObject;
private var _hasCalledInit: Boolean = false; private var _hasCalledInit: Boolean = false;
private val _hasInit: Boolean; private var _hasInit: Boolean = false;
private var _lastRequest: Long = Long.MIN_VALUE; private var _lastRequest: Long = Long.MIN_VALUE;
private val _hasOnConcluded: Boolean; private var _hasOnConcluded: Boolean = false;
override var nextRequest: Int = 1000 override var nextRequest: Int = 1000
private set; private set;
constructor(config: IV8PluginConfig, obj: V8ValueObject) { constructor(client: JSClient, obj: V8ValueObject) {
warnIfMainThread("JSPlaybackTracker.constructor"); warnIfMainThread("JSPlaybackTracker.constructor");
if(!obj.has("onProgress"))
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
if(!obj.has("nextRequest"))
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
_hasOnConcluded = obj.has("onConcluded");
this._config = config; client.busy {
this._obj = obj; if (!obj.has("onProgress"))
this._hasInit = obj.has("onInit"); throw ScriptImplementationException(
client.config,
"Missing onProgress on PlaybackTracker"
);
if (!obj.has("nextRequest"))
throw ScriptImplementationException(
client.config,
"Missing nextRequest on PlaybackTracker"
);
_hasOnConcluded = obj.has("onConcluded");
this._client = client;
this._config = client.config;
this._obj = obj;
this._hasInit = obj.has("onInit");
}
} }
override fun onInit(seconds: Double) { override fun onInit(seconds: Double) {
@@ -40,12 +54,15 @@ class JSPlaybackTracker: IPlaybackTracker {
synchronized(_obj) { synchronized(_obj) {
if(_hasCalledInit) if(_hasCalledInit)
return; return;
if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})"); _client.busy {
_obj.invokeVoid("onInit", seconds); if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeV8Void("onInit", seconds);
}
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true;
} }
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true;
} }
} }
@@ -55,10 +72,12 @@ class JSPlaybackTracker: IPlaybackTracker {
if(!_hasCalledInit && _hasInit) if(!_hasCalledInit && _hasInit)
onInit(seconds); onInit(seconds);
else { else {
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); _client.busy {
_obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); _obj.invokeV8Void("onProgress", Math.floor(seconds), isPlaying);
_lastRequest = System.currentTimeMillis(); nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_lastRequest = System.currentTimeMillis();
}
} }
} }
} }
@@ -67,7 +86,9 @@ class JSPlaybackTracker: IPlaybackTracker {
if(_hasOnConcluded) { if(_hasOnConcluded) {
synchronized(_obj) { synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded"); Logger.i("JSPlaybackTracker", "onConcluded");
_obj.invokeVoid("onConcluded", -1); _client.busy {
_obj.invokeV8Void("onConcluded", -1);
}
} }
} }
} }
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails { class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
@@ -68,12 +69,12 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
return null; return null;
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager); return JSContentPager(_pluginConfig, client, contentPager);
} }
private fun getCommentsJS(client: JSClient): JSCommentPager { private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>()); val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager); return JSCommentPager(_pluginConfig, client, commentPager);
} }
@@ -14,6 +14,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow 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.logging.Logger
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -46,52 +48,55 @@ class JSRequestExecutor {
if (_executor.isClosed) if (_executor.isClosed)
throw IllegalStateException("Executor object is closed"); throw IllegalStateException("Executor object is closed");
val result = if(_plugin is DevJSClient) return _plugin.getUnderlyingPlugin().busy {
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>( val result = if(_plugin is DevJSClient)
_config, StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
"[${_config.name}] JSRequestExecutor", V8Plugin.catchScriptErrors<Any>(
"builder.modifyRequest()" _config,
) { "[${_config.name}] JSRequestExecutor",
_executor.invoke("executeRequest", url, headers, method, body); "builder.modifyRequest()"
} as V8Value; ) {
} _executor.invokeV8("executeRequest", url, headers, method, body);
} as V8Value;
}
else V8Plugin.catchScriptErrors<Any>( else V8Plugin.catchScriptErrors<Any>(
_config, _config,
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invoke("executeRequest", url, headers, method, body); _executor.invokeV8("executeRequest", url, headers, method, body);
} as V8Value; } as V8Value;
try { try {
if(result is V8ValueString) { if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value); val base64Result = Base64.getDecoder().decode(result.value);
return base64Result; return@busy base64Result;
}
if(result is V8ValueTypedArray) {
val buffer = result.buffer;
val byteBuffer = buffer.byteBuffer;
val bytesResult = ByteArray(result.byteLength);
byteBuffer.get(bytesResult, 0, result.byteLength);
buffer.close();
return bytesResult;
}
if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
when(type) {
//TODO: Buffer type?
} }
if(result is V8ValueTypedArray) {
val buffer = result.buffer;
val byteBuffer = buffer.byteBuffer;
val bytesResult = ByteArray(result.byteLength);
byteBuffer.get(bytesResult, 0, result.byteLength);
buffer.close();
return@busy bytesResult;
}
if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
when(type) {
//TODO: Buffer type?
}
}
if(result is V8ValueUndefined) {
if(_plugin is DevJSClient)
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
}
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
} }
if(result is V8ValueUndefined) { finally {
if(_plugin is DevJSClient) result.close();
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
} }
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
}
finally {
result.close();
} }
} }
@@ -99,24 +104,25 @@ class JSRequestExecutor {
open fun cleanup() { open fun cleanup() {
if (!hasCleanup || _executor.isClosed) if (!hasCleanup || _executor.isClosed)
return; return;
_plugin.busy {
if(_plugin is DevJSClient) if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>( V8Plugin.catchScriptErrors<Any>(
_config, _config,
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invokeVoid("cleanup", null); _executor.invokeV8("cleanup", null);
}; };
} }
else V8Plugin.catchScriptErrors<Any>( else V8Plugin.catchScriptErrors<Any>(
_config, _config,
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invokeVoid("cleanup", null); _executor.invokeV8("cleanup", null);
}; };
}
} }
protected fun finalize() { protected fun finalize() {
@@ -11,12 +11,14 @@ import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSRequestModifier: IRequestModifier { class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient; private val _plugin: JSClient;
private val _config: IV8PluginConfig; private val _config: IV8PluginConfig;
private var _modifier: V8ValueObject; private var _modifier: V8ValueObject;
override var allowByteSkip: Boolean; override var allowByteSkip: Boolean = false;
constructor(plugin: JSClient, modifier: V8ValueObject) { constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin; this._plugin = plugin;
@@ -24,10 +26,13 @@ class JSRequestModifier: IRequestModifier {
this._config = plugin.config; this._config = plugin.config;
val config = plugin.config; val config = plugin.config;
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true; plugin.busy {
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
} }
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest { override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
@@ -35,13 +40,15 @@ class JSRequestModifier: IRequestModifier {
return Request(url, headers); return Request(url, headers);
} }
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { return _plugin.busy {
_modifier.invoke("modifyRequest", url, headers); val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
} as V8ValueObject; _modifier.invokeV8("modifyRequest", url, headers);
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers); val req = JSRequest(_plugin, result, url, headers);
result.close(); result.close();
return req; return@busy req;
}
} }
@@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getSourcePlugin
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -35,8 +37,11 @@ class JSSubtitleSource : ISubtitleSource {
override fun getSubtitles(): String { override fun getSubtitles(): String {
if(!hasFetch) if(!hasFetch)
throw IllegalStateException("This subtitle doesn't support getSubtitles.."); throw IllegalStateException("This subtitle doesn't support getSubtitles..");
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>());
return v8String.value; return _obj.getSourcePlugin()?.busy {
val v8String = _obj.invokeV8<V8ValueString>("getSubtitles", arrayOf<Any>());
return@busy v8String.value;
} ?: "";
} }
override suspend fun getSubtitlesURI(): Uri? { override suspend fun getSubtitlesURI(): Uri? {
@@ -24,9 +24,11 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
class JSVideoDetails : JSVideo, IPlatformVideoDetails { class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private val _plugin: JSClient;
private val _hasGetComments: Boolean; private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean; private val _hasGetContentRecommendations: Boolean;
private val _hasGetPlaybackTracker: Boolean; private val _hasGetPlaybackTracker: Boolean;
@@ -48,6 +50,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
val contextName = "VideoDetails"; val contextName = "VideoDetails";
_plugin = plugin;
val config = plugin.config; val config = plugin.config;
description = _content.getOrThrow(config, "description", contextName); description = _content.getOrThrow(config, "description", contextName);
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
@@ -82,14 +85,16 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return getPlaybackTrackerJS(); return getPlaybackTrackerJS();
} }
private fun getPlaybackTrackerJS(): IPlaybackTracker? { private fun getPlaybackTrackerJS(): IPlaybackTracker? {
return V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") { return _plugin.busy {
val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>()) V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
?: return@catchScriptErrors null; val tracker = _content.invokeV8<V8Value>("getPlaybackTracker", arrayOf<Any>())
if(tracker is V8ValueObject) ?: return@catchScriptErrors null;
return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker); if(tracker is V8ValueObject)
else return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
return@catchScriptErrors null; else
}; return@catchScriptErrors null;
}
}
} }
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? { override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
@@ -106,8 +111,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return null; return null;
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); return _plugin.busy {
return JSContentPager(_pluginConfig, client, contentPager); val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return@busy JSContentPager(_pluginConfig, client, contentPager);
}
} }
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? { override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
@@ -123,10 +130,12 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
} }
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? { private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>()); return _plugin.busy {
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? val commentPager = _content.invokeV8<V8Value>("getComments", arrayOf<Any>());
return null; if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return@busy null;
return JSCommentPager(_pluginConfig, client, commentPager); return@busy JSCommentPager(_pluginConfig, client, commentPager);
}
} }
} }
@@ -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.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource { class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
override val licenseUri: String override val licenseUri: String
@@ -25,7 +27,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
return null return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { 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) if (result !is V8ValueObject)
@@ -1,6 +1,8 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources 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.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.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource 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.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow 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.others.Language
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource { class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String; override val container : String;
@@ -50,6 +57,44 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
hasGenerate = _obj.has("generate"); 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? { override fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
@@ -62,21 +107,27 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
if(_plugin is DevJSClient) if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate"); _plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8<V8ValueString>("generate").value;
}
} }
} }
else else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate"); _plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8<V8ValueString>("generate").value;
}
} }
if(result != null){ if(result != null){
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; plugin.busy {
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
} }
} }
return result; 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.V8Value
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject 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.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
@@ -15,11 +16,18 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
interface IJSDashManifestRawSource { interface IJSDashManifestRawSource {
val hasGenerate: Boolean; val hasGenerate: Boolean;
var manifest: String?; var manifest: String?;
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
fun generate(): String?; fun generate(): String?;
} }
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource { open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
@@ -32,7 +40,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
override val duration: Long; override val duration: Long;
override val priority: Boolean; override val priority: Boolean;
var url: String?; val url: String?;
override var manifest: String?; override var manifest: String?;
override val hasGenerate: Boolean; override val hasGenerate: Boolean;
@@ -57,6 +65,45 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
hasGenerate = _obj.has("generate"); 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? { override open fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
@@ -67,22 +114,28 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
if(_plugin is DevJSClient) { if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate"); _plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8<V8ValueString>("generate").value;
}
}); });
} }
} }
else else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate"); _plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8<V8ValueString>("generate").value;
}
}); });
if(result != null){ if(result != null){
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; _plugin.busy {
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
} }
} }
return result; return result;
@@ -110,6 +163,32 @@ class JSDashManifestMergingRawSource(
override val priority: Boolean override val priority: Boolean
get() = video.priority; 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? { override fun generate(): String? {
val videoDash = video.generate(); val videoDash = video.generate();
val audioDash = audio.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.engine.V8Plugin
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Void
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource, class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
IDashManifestWidevineSource, JSSource { IDashManifestWidevineSource, JSSource {
@@ -45,7 +47,7 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
return null return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") { 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) if (result !is V8ValueObject)
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
@@ -38,7 +39,13 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
companion object { companion object {
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }; fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? {
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj); obj?.ensureIsBusy();
return obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }
};
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource {
obj.ensureIsBusy();
return JSHLSManifestAudioSource(plugin, obj)
};
} }
} }
@@ -14,7 +14,9 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
@@ -53,36 +55,39 @@ abstract class JSSource {
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor"); hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
} }
fun getRequestModifier(): IRequestModifier? { fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") {
if(_requestModifier != null) if(_requestModifier != null)
return AdhocRequestModifier { url, headers -> return@isBusyWith AdhocRequestModifier { url, headers ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers); return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
}; };
if (!hasRequestModifier || _obj.isClosed) if (!hasRequestModifier || _obj.isClosed)
return null; return@isBusyWith null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { 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) if (result !is V8ValueObject)
return null; return@isBusyWith null;
return JSRequestModifier(_plugin, result) return@isBusyWith JSRequestModifier(_plugin, result)
} }
open fun getRequestExecutor(): JSRequestExecutor? { open fun getRequestExecutor(): JSRequestExecutor? = _plugin.isBusyWith("getRequestExecutor") {
if (!hasRequestExecutor || _obj.isClosed) if (!hasRequestExecutor || _obj.isClosed)
return null; return@isBusyWith null;
Logger.v("JSSource", "Request executor for [${type}] requesting");
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestExecutor", arrayOf<Any>()); _obj.invokeV8("getRequestExecutor", arrayOf<Any>());
}; };
if (result !is V8ValueObject) Logger.v("JSSource", "Request executor for [${type}] received");
return null;
return JSRequestExecutor(_plugin, result) if (result !is V8ValueObject)
return@isBusyWith null;
return@isBusyWith JSRequestExecutor(_plugin, result)
} }
fun getUnderlyingPlugin(): JSClient? { fun getUnderlyingPlugin(): JSClient? {
@@ -105,8 +110,12 @@ abstract class JSSource {
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource" const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource" const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) }; fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? {
obj?.ensureIsBusy();
return obj.orNull { fromV8Video(plugin, it as V8ValueObject) }
};
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? { fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
obj.ensureIsBusy()
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj); TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
@@ -123,13 +132,26 @@ abstract class JSSource {
} }
} }
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) }; fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj); fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource{
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj); obj.ensureIsBusy();
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj); return JSDashManifestSource(plugin, obj)
};
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource{
obj.ensureIsBusy()
return JSDashManifestRawSource(plugin, obj);
}
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource {
obj?.ensureIsBusy();
return JSDashManifestRawAudioSource(plugin, obj)
};
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }; fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj); fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource {
obj.ensureIsBusy();
return JSHLSManifestSource(plugin, obj)
};
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? { fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? {
obj.ensureIsBusy();
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj); TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor { class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
@@ -31,6 +32,7 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor { fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor {
obj.ensureIsBusy();
val type = obj.getString("plugin_type") val type = obj.getString("plugin_type")
return when(type) { return when(type) {
TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj); TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj);
@@ -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.api.media.platforms.js.models.JSRequestExecutor
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource { class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
override val licenseUri: String override val licenseUri: String
@@ -25,7 +26,7 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
return null return null
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { 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) if (result !is V8ValueObject)
@@ -35,7 +35,7 @@ class ChromecastCastingDevice : CastingDevice {
override var usedRemoteAddress: InetAddress? = null; override var usedRemoteAddress: InetAddress? = null;
override var localAddress: InetAddress? = null; override var localAddress: InetAddress? = null;
override val canSetVolume: Boolean get() = true; override val canSetVolume: Boolean get() = true;
override val canSetSpeed: Boolean get() = false; //TODO: Implement override val canSetSpeed: Boolean get() = true;
var addresses: Array<InetAddress>? = null; var addresses: Array<InetAddress>? = null;
var port: Int = 0; var port: Int = 0;
@@ -144,6 +144,23 @@ class ChromecastCastingDevice : CastingDevice {
sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json); sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json);
} }
override fun changeSpeed(speed: Double) {
if (invokeInIOScopeIfRequired { changeSpeed(speed) }) return
val speedClamped = speed.coerceAtLeast(1.0).coerceAtLeast(1.0).coerceAtMost(2.0)
setSpeed(speedClamped)
val mediaSessionId = _mediaSessionId ?: return
val transportId = _transportId ?: return
val setSpeedObject = JSONObject().apply {
put("type", "SET_PLAYBACK_RATE")
put("mediaSessionId", mediaSessionId)
put("playbackRate", speedClamped)
put("requestId", _requestId++)
}
sendChannelMessage(sourceId = "sender-0", destinationId = transportId, namespace = "urn:x-cast:com.google.cast.media", json = setSpeedObject.toString())
}
override fun changeVolume(volume: Double) { override fun changeVolume(volume: Double) {
if (invokeInIOScopeIfRequired({ changeVolume(volume) })) { if (invokeInIOScopeIfRequired({ changeVolume(volume) })) {
return; return;
@@ -64,6 +64,7 @@ import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Collections import java.util.Collections
import java.util.UUID import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
class StateCasting { class StateCasting {
private val _scopeIO = CoroutineScope(Dispatchers.IO); private val _scopeIO = CoroutineScope(Dispatchers.IO);
@@ -89,6 +90,7 @@ class StateCasting {
var _resumeCastingDevice: CastingDeviceInfo? = null; var _resumeCastingDevice: CastingDeviceInfo? = null;
private var _nsdManager: NsdManager? = null private var _nsdManager: NsdManager? = null
val isCasting: Boolean get() = activeDevice != null; val isCasting: Boolean get() = activeDevice != null;
private val _castId = AtomicInteger(0)
private val _discoveryListeners = mapOf( private val _discoveryListeners = mapOf(
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice), "_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
@@ -432,129 +434,112 @@ class StateCasting {
action(); action();
} }
fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean { fun cancel() {
val ad = activeDevice ?: return false; _castId.incrementAndGet()
if (ad.connectionState != CastConnectionState.CONNECTED) { }
return false;
}
val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean {
return withContext(Dispatchers.IO) {
val ad = activeDevice ?: return@withContext false;
if (ad.connectionState != CastConnectionState.CONNECTED) {
return@withContext false;
}
var sourceCount = 0; val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0;
if (videoSource != null) sourceCount++; val castId = _castId.incrementAndGet()
if (audioSource != null) sourceCount++;
if (subtitleSource != null) sourceCount++;
if (sourceCount < 1) { var sourceCount = 0;
throw Exception("At least one source should be specified."); if (videoSource != null) sourceCount++;
} if (audioSource != null) sourceCount++;
if (subtitleSource != null) sourceCount++;
if (sourceCount > 1) { if (sourceCount < 1) {
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { throw Exception("At least one source should be specified.");
if (ad is AirPlayCastingDevice) { }
Logger.i(TAG, "Casting as local HLS");
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); if (sourceCount > 1) {
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) {
if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as local HLS");
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as local DASH");
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed);
}
} else { } else {
Logger.i(TAG, "Casting as local DASH"); val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); if (isRawDash) {
} Logger.i(TAG, "Casting as raw DASH");
} else {
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
if (isRawDash) {
Logger.i(TAG, "Casting as raw DASH");
try { castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading);
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed); } else {
} catch (e: Throwable) { if (ad is FCastCastingDevice) {
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e); Logger.i(TAG, "Casting as DASH direct");
} castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as HLS indirect");
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else { } else {
if (ad is FCastCastingDevice) { Logger.i(TAG, "Casting as DASH indirect");
Logger.i(TAG, "Casting as DASH direct"); castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as HLS indirect");
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as DASH indirect");
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
}
} }
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
}
}
}
} else {
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
val url = getLocalUrl(ad);
val id = UUID.randomUUID();
if (videoSource is IVideoUrlSource) {
val videoPath = "/video-${id}"
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
Logger.i(TAG, "Casting as singular video");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
} else if (audioSource is IAudioUrlSource) {
val audioPath = "/audio-${id}"
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
Logger.i(TAG, "Casting as singular audio");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
} else if(videoSource is IHLSManifestSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied HLS");
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as non-proxied HLS");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
}
} else if(audioSource is IHLSManifestAudioSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied audio HLS");
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as non-proxied audio HLS");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed);
}
} else if (videoSource is LocalVideoSource) {
Logger.i(TAG, "Casting as local video");
castLocalVideo(video, videoSource, resumePosition, speed);
} else if (audioSource is LocalAudioSource) {
Logger.i(TAG, "Casting as local audio");
castLocalAudio(video, audioSource, resumePosition, speed);
} else if (videoSource is JSDashManifestRawSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
}
}
} else if (audioSource is JSDashManifestRawAudioSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
StateApp.instance.scope.launch(Dispatchers.IO) {
try {
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
} }
} }
} else { } else {
var str = listOf( val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, val url = getLocalUrl(ad);
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, val id = UUID.randomUUID();
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
).filterNotNull().joinToString(", ");
throw UnsupportedCastException(str);
}
}
return true; if (videoSource is IVideoUrlSource) {
val videoPath = "/video-${id}"
val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl();
Logger.i(TAG, "Casting as singular video");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed);
} else if (audioSource is IAudioUrlSource) {
val audioPath = "/audio-${id}"
val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl();
Logger.i(TAG, "Casting as singular audio");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed);
} else if(videoSource is IHLSManifestSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied HLS");
castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as non-proxied HLS");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed);
}
} else if(audioSource is IHLSManifestAudioSource) {
if (proxyStreams || ad is ChromecastCastingDevice) {
Logger.i(TAG, "Casting as proxied audio HLS");
castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed);
} else {
Logger.i(TAG, "Casting as non-proxied audio HLS");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed);
}
} else if (videoSource is LocalVideoSource) {
Logger.i(TAG, "Casting as local video");
castLocalVideo(video, videoSource, resumePosition, speed);
} else if (audioSource is LocalAudioSource) {
Logger.i(TAG, "Casting as local audio");
castLocalAudio(video, audioSource, resumePosition, speed);
} else if (videoSource is JSDashManifestRawSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
} else if (audioSource is JSDashManifestRawAudioSource) {
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed, castId, onLoadingEstimate, onLoading);
} else {
var str = listOf(
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null,
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
).filterNotNull().joinToString(", ");
throw UnsupportedCastException(str);
}
}
return@withContext true;
}
} }
fun resumeVideo(): Boolean { fun resumeVideo(): Boolean {
@@ -1236,7 +1221,7 @@ class StateCasting {
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> { private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?, castId: Int, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
cleanExecutors() cleanExecutors()
@@ -1283,20 +1268,48 @@ class StateCasting {
} }
} }
var dashContent = withContext(Dispatchers.IO) { var dashContent: String = withContext(Dispatchers.IO) {
stopVideo()
//TODO: Include subtitlesURl in the future //TODO: Include subtitlesURl in the future
return@withContext if (audioSource != null && videoSource != null) { val deferred = if (audioSource != null && videoSource != null) {
JSDashManifestMergingRawSource(videoSource, audioSource).generate() JSDashManifestMergingRawSource(videoSource, audioSource).generateAsync(_scopeIO)
} else if (audioSource != null) { } else if (audioSource != null) {
audioSource.generate() audioSource.generateAsync(_scopeIO)
} else if (videoSource != null) { } else if (videoSource != null) {
videoSource.generate() videoSource.generateAsync(_scopeIO)
} else { } else {
Logger.e(TAG, "Expected at least audio or video to be set") Logger.e(TAG, "Expected at least audio or video to be set")
null null
} }
if (deferred != null) {
try {
withContext(Dispatchers.Main) {
if (deferred.estDuration >= 0) {
onLoadingEstimate?.invoke(deferred.estDuration)
} else {
onLoading?.invoke(true)
}
}
deferred.await()
} finally {
if (castId == _castId.get()) {
withContext(Dispatchers.Main) {
onLoading?.invoke(false)
}
}
}
} else {
return@withContext null
}
} ?: throw Exception("Dash is null") } ?: throw Exception("Dash is null")
if (castId != _castId.get()) {
Log.i(TAG, "Get DASH cancelled.")
return emptyList()
}
for (representation in representationRegex.findAll(dashContent)) { for (representation in representationRegex.findAll(dashContent)) {
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found") val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
dashContent = mediaInitializationRegex.replace(dashContent) { dashContent = mediaInitializationRegex.replace(dashContent) {
@@ -6,12 +6,16 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Button import android.widget.Button
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
import com.futo.platformplayer.readBytes import com.futo.platformplayer.readBytes
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ImportOptionsDialog: AlertDialog { class ImportOptionsDialog: AlertDialog {
private val _context: MainActivity; private val _context: MainActivity;
@@ -41,8 +45,17 @@ class ImportOptionsDialog: AlertDialog {
_button_import_zip.onClick.subscribe { _button_import_zip.onClick.subscribe {
dismiss(); dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/zip") { StateApp.instance.requestFileReadAccess(_context, null, "application/zip") {
val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess; StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes); 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 { _button_import_ezip.setOnClickListener {
@@ -51,17 +64,35 @@ class ImportOptionsDialog: AlertDialog {
_button_import_txt.onClick.subscribe { _button_import_txt.onClick.subscribe {
dismiss(); dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "text/plain") { StateApp.instance.requestFileReadAccess(_context, null, "text/plain") {
val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess; StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val txt = String(txtBytes); val txtBytes = it?.readBytes(context) ?: return@launch;
StateBackup.importTxt(_context, txt); 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 { _button_import_newpipe_subs.onClick.subscribe {
dismiss(); dismiss();
StateApp.instance.requestFileReadAccess(_context, null, "application/json") { StateApp.instance.requestFileReadAccess(_context, null, "application/json") {
val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess; StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val json = String(jsonBytes); val jsonBytes = it?.readBytes(context) ?: return@launch;
StateBackup.importNewPipeSubs(_context, json); 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 { _button_import_platform.onClick.subscribe {
@@ -724,7 +724,7 @@ class VideoDownload {
val t = cue.groupValues[1]; val t = cue.groupValues[1];
val d = cue.groupValues[2]; val d = cue.groupValues[2];
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter + 1).toString()); val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
val data = if(executor != null) val data = if(executor != null)
executor.executeRequest("GET", url, null, mapOf()); executor.executeRequest("GET", url, null, mapOf());
@@ -6,13 +6,13 @@ import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.interop.V8Runtime
import com.caoccao.javet.interop.options.V8Flags
import com.caoccao.javet.interop.options.V8RuntimeOptions
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString 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.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
@@ -26,6 +26,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.engine.internal.V8Converter import com.futo.platformplayer.engine.internal.V8Converter
@@ -38,8 +39,18 @@ import com.futo.platformplayer.engine.packages.V8Package
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets 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 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.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class V8Plugin { class V8Plugin {
val config: IV8PluginConfig; val config: IV8PluginConfig;
@@ -47,10 +58,14 @@ class V8Plugin {
private val _clientAuth: ManagedHttpClient; private val _clientAuth: ManagedHttpClient;
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap(); private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
private val _promises = ConcurrentHashMap<V8ValuePromise, ((V8ValuePromise)->Unit)?>();
val httpClient: ManagedHttpClient get() = _client; val httpClient: ManagedHttpClient get() = _client;
val httpClientAuth: ManagedHttpClient get() = _clientAuth; val httpClientAuth: ManagedHttpClient get() = _clientAuth;
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers; val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
var runtimeId: Int = 0;
fun registerHttpClient(client: JSHttpClient) { fun registerHttpClient(client: JSHttpClient) {
synchronized(_clientOthers) { synchronized(_clientOthers) {
_clientOthers.put(client.clientId, client); _clientOthers.put(client.clientId, client);
@@ -67,10 +82,8 @@ class V8Plugin {
var isStopped = true; var isStopped = true;
val onStopped = Event1<V8Plugin>(); val onStopped = Event1<V8Plugin>();
//TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial private val _busyLock = ReentrantLock()
private val _busyCounterLock = Object(); val isBusy get() = _busyLock.isLocked;
private var _busyCounter = 0;
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
var allowDevSubmit: Boolean = false var allowDevSubmit: Boolean = false
private set(value) { private set(value) {
@@ -140,6 +153,7 @@ class V8Plugin {
synchronized(_runtimeLock) { synchronized(_runtimeLock) {
if (_runtime != null) if (_runtime != null)
return; return;
runtimeId = runtimeId + 1;
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true); //V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
val host = V8Host.getV8Instance(); val host = V8Host.getV8Instance();
val options = host.jsRuntimeType.getRuntimeOptions(); val options = host.jsRuntimeType.getRuntimeOptions();
@@ -148,6 +162,8 @@ class V8Plugin {
if (!host.isIsolateCreated) if (!host.isIsolateCreated)
throw IllegalStateException("Isolate not created"); throw IllegalStateException("Isolate not created");
_runtimeMap.put(_runtime!!, this);
//Setup bridge //Setup bridge
_runtime?.let { _runtime?.let {
it.converter = V8Converter(); it.converter = V8Converter();
@@ -184,10 +200,13 @@ class V8Plugin {
} }
fun stop(){ fun stop(){
Logger.i(TAG, "Stopping plugin [${config.name}]"); Logger.i(TAG, "Stopping plugin [${config.name}]");
isStopped = true; busy {
whenNotBusy { Logger.i(TAG, "Plugin stopping");
synchronized(_runtimeLock) { synchronized(_runtimeLock) {
if(isStopped)
return@busy;
isStopped = true; isStopped = true;
runtimeId = runtimeId + 1;
//Cleanup http //Cleanup http
for(pack in _depsPackages) { for(pack in _depsPackages) {
@@ -197,6 +216,7 @@ class V8Plugin {
} }
_runtime?.let { _runtime?.let {
_runtimeMap.remove(it);
_runtime = null; _runtime = null;
if(!it.isClosed && !it.isDead) { if(!it.isClosed && !it.isDead) {
try { try {
@@ -211,62 +231,147 @@ class V8Plugin {
Logger.i(TAG, "Stopped plugin [${config.name}]"); Logger.i(TAG, "Stopped plugin [${config.name}]");
}; };
} }
Logger.i(TAG, "Plugin stopped");
onStopped.emit(this); 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 { fun execute(js: String) : V8Value {
return executeTyped<V8Value>(js); 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 { fun <T : V8Value> executeTyped(js: String) : T {
warnIfMainThread("V8Plugin.executeTyped"); warnIfMainThread("V8Plugin.executeTyped");
if(isStopped) if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js); throw PluginEngineStoppedException(config, "Instance is stopped", js);
synchronized(_busyCounterLock) { val result = busy {
_busyCounter++; val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
} return@busy catchScriptErrors<V8Value>("Plugin[${config.name}]", js) {
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
try {
return catchScriptErrors("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute() runtime.getExecutor(js).execute()
}; };
};
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this@V8Plugin);
} }
finally { return result as T;
synchronized(_busyCounterLock) {
//Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
try {
afterBusy.emit(_busyCounter - 1);
}
catch(ex: Throwable) {
Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex);
}
_busyCounter--;
}
}
} }
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value }; fun executeBoolean(js: String) : Boolean? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value } }
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value }; fun executeString(js: String) : String? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value } }
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value }; fun executeInteger(js: String) : Int? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value } }
fun whenNotBusy(handler: (V8Plugin)->Unit) {
synchronized(_busyCounterLock) { fun <T: V8Value> handlePromise(result: V8ValuePromise): CompletableDeferred<T> {
if(_busyCounter == 0) val def = CompletableDeferred<T>();
handler(this); result.register(object: IV8ValuePromise.IListener {
else { override fun onFulfilled(p0: V8Value?) {
val tag = Object(); resolvePromise(result);
afterBusy.subscribe(tag) { def.complete(p0 as T);
if(it == 0) {
Logger.w(TAG, "V8Plugin afterBusy handled");
afterBusy.remove(tag);
handler(this);
}
}
} }
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? { private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
//TODO: Auto get all package types? //TODO: Auto get all package types?
return when(packageName) { return when(packageName) {
@@ -292,8 +397,14 @@ class V8Plugin {
private val REGEX_EX_FALLBACK = Regex(".*throw.*?[\"](.*)[\"].*"); private val REGEX_EX_FALLBACK = Regex(".*throw.*?[\"](.*)[\"].*");
private val REGEX_EX_FALLBACK2 = Regex(".*throw.*?['](.*)['].*"); private val REGEX_EX_FALLBACK2 = Regex(".*throw.*?['](.*)['].*");
private val _runtimeMap = ConcurrentHashMap<V8Runtime, V8Plugin>();
val TAG = "V8Plugin"; val TAG = "V8Plugin";
fun getPluginFromRuntime(runtime: V8Runtime): V8Plugin? {
return _runtimeMap.getOrDefault(runtime, null);
}
fun <T: Any?> catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T { fun <T: Any?> catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T {
var codeStripped = code; var codeStripped = code;
if(codeStripped != null) { //TODO: Improve code stripped if(codeStripped != null) { //TODO: Improve code stripped
@@ -327,14 +438,23 @@ class V8Plugin {
throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped); throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
} }
catch(executeEx: JavetExecutionException) { catch(executeEx: JavetExecutionException) {
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) { val obj = executeEx.scriptingError?.context
val pluginType = executeEx.scriptingError.context["plugin_type"].toString(); if(obj != null && obj.containsKey("plugin_type") == true) {
val pluginType = obj["plugin_type"].toString();
//Captcha //Captcha
if (pluginType == "CaptchaRequiredException") { if (pluginType == "CaptchaRequiredException") {
throw ScriptCaptchaRequiredException(config, throw ScriptCaptchaRequiredException(config,
executeEx.scriptingError.context["url"]?.toString(), obj["url"]?.toString(),
executeEx.scriptingError.context["body"]?.toString(), obj["body"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped);
}
//Reload Required
if (pluginType == "ReloadRequiredException") {
throw ScriptReloadRequiredException(config,
obj["msg"]?.toString(),
obj["reloadData"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped); executeEx, executeEx.scriptingError?.stack, codeStripped);
} }
@@ -348,6 +468,41 @@ class V8Plugin {
codeStripped codeStripped
); );
} }
/* //Required for newer V8 versions
if(executeEx.scriptingError?.context is IJavetEntityError) {
val obj = executeEx.scriptingError?.context as IJavetEntityError
if(obj.context.containsKey("plugin_type") == true) {
val pluginType = obj.context["plugin_type"].toString();
//Captcha
if (pluginType == "CaptchaRequiredException") {
throw ScriptCaptchaRequiredException(config,
obj.context["url"]?.toString(),
obj.context["body"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped);
}
//Reload Required
if (pluginType == "ReloadRequiredException") {
throw ScriptReloadRequiredException(config,
obj.context["msg"]?.toString(),
obj.context["reloadData"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped);
}
//Others
throwExceptionFromV8(
config,
pluginType,
(extractJSExceptionMessage(executeEx) ?: ""),
executeEx,
executeEx.scriptingError?.stack,
codeStripped
);
}
}
*/
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped); throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
} }
catch(ex: Exception) { catch(ex: Exception) {
@@ -398,9 +553,4 @@ class V8Plugin {
return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found"); return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found");
} }
} }
/**
* Methods available for scripts (bridge object)
*/
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class NoInternetException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { open class NoInternetException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
@@ -11,6 +12,7 @@ open class NoInternetException(config: IV8PluginConfig, error: String, ex: Excep
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : NoInternetException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : NoInternetException {
obj.ensureIsBusy();
return NoInternetException(config, obj.getOrThrow(config, "message", "NoInternetException")); return NoInternetException(config, obj.getOrThrow(config, "message", "NoInternetException"));
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
@@ -11,6 +12,7 @@ open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Except
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException")); return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException"));
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -9,6 +10,7 @@ class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String?,
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
val contextName = "ScriptCaptchaRequiredException"; val contextName = "ScriptCaptchaRequiredException";
return ScriptCaptchaRequiredException(config, return ScriptCaptchaRequiredException(config,
obj.getOrDefault<String>(config, "url", contextName, null), obj.getOrDefault<String>(config, "url", contextName, null),
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) { class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException {
obj.ensureIsBusy();
return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException")); return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException"));
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
@@ -11,6 +12,7 @@ open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: E
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException")); return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException"));
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptExecutionException(config, error, ex, stack, code) { open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptExecutionException(config, error, ex, stack, code) {
@@ -11,6 +12,7 @@ open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException")); return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException"));
} }
} }
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex: Exception? = null, val stack: String? = null, code: String? = null) : PluginException(config, error, ex, code) { open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex: Exception? = null, val stack: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
@@ -11,6 +12,7 @@ open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex:
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException {
obj.ensureIsBusy();
return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException")); return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException"));
} }
} }
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptImplementationException(config: IV8PluginConfig, error: String, ex: Exception? = null, var pluginId: String? = null, code: String? = null) : PluginException(config, error, ex, code) { class ScriptImplementationException(config: IV8PluginConfig, error: String, ex: Exception? = null, var pluginId: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException {
obj.ensureIsBusy();
return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException")); return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException"));
} }
} }
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException")); return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException"));
} }
} }
@@ -0,0 +1,22 @@
package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
class ScriptReloadRequiredException(config: IV8PluginConfig, val msg: String?, val reloadData: String?, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, msg ?: "ReloadRequired", ex, stack, code) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
val contextName = "ScriptReloadRequiredException";
return ScriptReloadRequiredException(config,
obj.getOrThrow(config, "message", contextName),
obj.getOrDefault<String>(config, "reloadData", contextName, null));
}
}
}
@@ -2,11 +2,13 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) { class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException {
obj.ensureIsBusy();
return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException")); return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException"));
} }
} }
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptUnavailableException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { class ScriptUnavailableException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException")); return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException"));
} }
} }
@@ -13,8 +13,8 @@ open class V8BindObject : IV8Convertable {
override fun toV8(runtime: V8Runtime): V8Value? { override fun toV8(runtime: V8Runtime): V8Value? {
synchronized(this) { synchronized(this) {
if(_runtimeObj != null) //if(_runtimeObj != null)
return _runtimeObj; // return _runtimeObj;
val v8Obj = runtime.createV8ValueObject(); val v8Obj = runtime.createV8ValueObject();
v8Obj.bind(this); v8Obj.bind(this);
@@ -4,6 +4,7 @@ import android.media.MediaCodec
import android.media.MediaCodecList import android.media.MediaCodecList
import com.caoccao.javet.annotations.V8Function import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property import com.caoccao.javet.annotations.V8Property
import com.caoccao.javet.interop.callback.JavetCallbackContext
import com.caoccao.javet.utils.JavetResourceUtils import com.caoccao.javet.utils.JavetResourceUtils
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueFunction import com.caoccao.javet.values.reference.V8ValueFunction
@@ -26,6 +27,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.concurrent.ConcurrentHashMap
class PackageBridge : V8Package { class PackageBridge : V8Package {
@Transient @Transient
@@ -78,6 +80,15 @@ class PackageBridge : V8Package {
return "android"; return "android";
} }
@V8Property
fun supportedFeatures(): Array<String> {
return arrayOf(
"ReloadRequiredException",
"HttpBatchClient",
"Async"
);
}
@V8Property @V8Property
fun supportedContent(): Array<Int> { fun supportedContent(): Array<Int> {
return arrayOf( return arrayOf(
@@ -101,45 +112,54 @@ class PackageBridge : V8Package {
} }
var timeoutCounter = 0; var timeoutCounter = 0;
var timeoutMap = HashSet<Int>(); var timeoutMap = ConcurrentHashMap<Int, Any?>();
@V8Function @V8Function
fun setTimeout(func: V8ValueFunction, timeout: Long): Int { fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
val id = timeoutCounter++; val id = timeoutCounter++;
val funcClone = func.toClone<V8ValueFunction>() val funcClone = func.toClone<V8ValueFunction>()
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
delay(timeout); delay(timeout);
synchronized(timeoutMap) { if (_plugin.isStopped)
if(!timeoutMap.contains(id)) { return@launch;
JavetResourceUtils.safeClose(funcClone); if (!timeoutMap.containsKey(id)) {
return@launch; _plugin.busy {
if (!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone);
} }
timeoutMap.remove(id); return@launch;
} }
timeoutMap.remove(id);
try { try {
_plugin.whenNotBusy { Logger.w(TAG, "setTimeout before busy (${timeout}): ${_plugin.isBusy}");
funcClone.callVoid(null, arrayOf<Any>()); _plugin.busy {
Logger.w(TAG, "setTimeout in busy");
if (!_plugin.isStopped)
funcClone.callVoid(null, arrayOf<Any>());
Logger.w(TAG, "setTimeout after");
} }
} } catch (ex: Throwable) {
catch(ex: Throwable) {
Logger.e(TAG, "Failed timeout callback", ex); Logger.e(TAG, "Failed timeout callback", ex);
} } finally {
finally { _plugin.busy {
JavetResourceUtils.safeClose(funcClone); if (!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone);
}
//_plugin.whenNotBusy {
//}
} }
}; };
synchronized(timeoutMap) { timeoutMap.put(id, true);
timeoutMap.add(id);
}
return id; return id;
} }
@V8Function @V8Function
fun clearTimeout(id: Int) { fun clearTimeout(id: Int) {
synchronized(timeoutMap) { if (timeoutMap.containsKey(id))
if(timeoutMap.contains(id)) timeoutMap.remove(id);
timeoutMap.remove(id); }
} @V8Function
fun sleep(length: Int) {
Thread.sleep(length.toLong());
} }
@V8Function @V8Function
@@ -147,7 +167,7 @@ class PackageBridge : V8Package {
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}"); Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try { try {
UIDialogs.toast(str); UIDialogs.appToast(str);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e); Logger.e(TAG, "Failed to show toast.", e);
} }
@@ -17,6 +17,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.internal.IV8Convertable import com.futo.platformplayer.engine.internal.IV8Convertable
import com.futo.platformplayer.engine.internal.V8BindObject import com.futo.platformplayer.engine.internal.V8BindObject
import com.futo.platformplayer.invokeV8Void
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
@@ -44,6 +45,17 @@ class PackageHttp: V8Package {
private val aliveSockets = mutableListOf<SocketResult>(); private val aliveSockets = mutableListOf<SocketResult>();
private var _cleanedUp = false; private var _cleanedUp = false;
private val _clients = mutableMapOf<String, PackageHttpClient>()
fun getClient(id: String?): PackageHttpClient {
if(id == null)
throw IllegalArgumentException("Http client ${id} doesn't exist");
if(_packageClient.clientId() == id)
return _packageClient;
if(_packageClientAuth.clientId() == id)
return _packageClientAuth;
return _clients.getOrDefault(id, null) ?: throw IllegalArgumentException("Http client ${id} doesn't exist");
}
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) { constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
_config = config; _config = config;
@@ -112,6 +124,8 @@ class PackageHttp: V8Package {
_plugin.registerHttpClient(httpClient); _plugin.registerHttpClient(httpClient);
val client = PackageHttpClient(this, httpClient); val client = PackageHttpClient(this, httpClient);
_clients.put(client.clientId() ?: "", client);
return client; return client;
} }
@V8Function @V8Function
@@ -246,18 +260,18 @@ class PackageHttp: V8Package {
@V8Function @V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder { fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequest(_package.getDefaultClient(useAuth), method, url, headers); return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers);
} }
@V8Function @V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder { fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequestWithBody(_package.getDefaultClient(useAuth), method, url, body, headers); return clientRequestWithBody(_package.getDefaultClient(useAuth).clientId(), method, url, body, headers);
} }
@V8Function @V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientGET(_package.getDefaultClient(useAuth), url, headers); = clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers);
@V8Function @V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers); = clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers);
@V8Function @V8Function
fun DUMMY(): BatchBuilder { fun DUMMY(): BatchBuilder {
@@ -268,21 +282,21 @@ class PackageHttp: V8Package {
//Client-specific //Client-specific
@V8Function @V8Function
fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder { fun clientRequest(clientId: String?, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(client, RequestDescriptor(method, url, headers))); _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers)));
return BatchBuilder(_package, _reqs); return BatchBuilder(_package, _reqs);
} }
@V8Function @V8Function
fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder { fun clientRequestWithBody(clientId: String?, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(client, RequestDescriptor(method, url, headers, body))); _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers, body)));
return BatchBuilder(_package, _reqs); return BatchBuilder(_package, _reqs);
} }
@V8Function @V8Function
fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder fun clientGET(clientId: String?, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequest(client, "GET", url, headers); = clientRequest(clientId, "GET", url, headers);
@V8Function @V8Function
fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder fun clientPOST(clientId: String?, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequestWithBody(client, "POST", url, body, headers); = clientRequestWithBody(clientId, "POST", url, body, headers);
//Finalizer //Finalizer
@@ -321,6 +335,7 @@ class PackageHttp: V8Package {
@Transient @Transient
private val _clientId: String?; private val _clientId: String?;
@V8Property @V8Property
fun clientId(): String? { fun clientId(): String? {
return _clientId; return _clientId;
@@ -333,6 +348,17 @@ class PackageHttp: V8Package {
_clientId = if(_client is JSHttpClient) _client.clientId else null; _clientId = if(_client is JSHttpClient) _client.clientId else null;
} }
@V8Function
fun resetAuthCookies(){
if(_client is JSHttpClient)
_client.resetAuthCookies();
}
@V8Function
fun clearOtherCookies(){
if(_client is JSHttpClient)
_client.clearOtherCookies();
}
@V8Function @V8Function
fun setDefaultHeaders(defaultHeaders: Map<String, String>) { fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
for(pair in defaultHeaders) for(pair in defaultHeaders)
@@ -429,8 +455,23 @@ class PackageHttp: V8Package {
}; };
} }
@V8Function @V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse {
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING) if(body is V8ValueString)
return POSTInternal(url, body.value, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is String)
return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is V8ValueTypedArray)
return POSTInternal(url, body.toBytes(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is ByteArray)
return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
return POSTInternal(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
}
// = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun POSTInternal(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { fun POSTInternal(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
@@ -452,9 +493,6 @@ class PackageHttp: V8Package {
} }
}; };
} }
@V8Function
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun POSTInternal(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { fun POSTInternal(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
@@ -630,7 +668,9 @@ class PackageHttp: V8Package {
_isOpen = true; _isOpen = true;
if(hasOpen && _listeners?.isClosed != true) { if(hasOpen && _listeners?.isClosed != true) {
try { try {
_listeners?.invokeVoid("open", arrayOf<Any>()); _package._plugin.busy {
_listeners?.invokeV8Void("open", arrayOf<Any>());
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
@@ -640,7 +680,9 @@ class PackageHttp: V8Package {
override fun message(msg: String) { override fun message(msg: String) {
if(hasMessage && _listeners?.isClosed != true) { if(hasMessage && _listeners?.isClosed != true) {
try { try {
_listeners?.invokeVoid("message", msg); _package._plugin.busy {
_listeners?.invokeV8Void("message", msg);
}
} }
catch(ex: Throwable) {} catch(ex: Throwable) {}
} }
@@ -649,7 +691,9 @@ class PackageHttp: V8Package {
if(hasClosing && _listeners?.isClosed != true) if(hasClosing && _listeners?.isClosed != true)
{ {
try { try {
_listeners?.invokeVoid("closing", code, reason); _package._plugin.busy {
_listeners?.invokeV8Void("closing", code, reason);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
@@ -660,7 +704,9 @@ class PackageHttp: V8Package {
_isOpen = false; _isOpen = false;
if(hasClosed && _listeners?.isClosed != true) { if(hasClosed && _listeners?.isClosed != true) {
try { try {
_listeners?.invokeVoid("closed", code, reason); _package._plugin.busy {
_listeners?.invokeV8Void("closed", code, reason);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
@@ -676,7 +722,9 @@ class PackageHttp: V8Package {
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception); Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
if(hasFailure && _listeners?.isClosed != true) { if(hasFailure && _listeners?.isClosed != true) {
try { try {
_listeners?.invokeVoid("failure", exception.message); _package._plugin.busy {
_listeners?.invokeV8Void("failure", exception.message);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
@@ -778,6 +778,8 @@ class ArticleDetailFragment : MainFragment {
view.onAddToWatchLaterClicked.subscribe { a -> view.onAddToWatchLaterClicked.subscribe { a ->
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
UIDialogs.toast("Added to watch later\n[${content.name}]") UIDialogs.toast("Added to watch later\n[${content.name}]")
else
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
} }
} }
else if(content is IPlatformPost) { else if(content is IPlatformPost) {
@@ -172,7 +172,7 @@ class ChannelFragment : MainFragment() {
_buttonSubscribe = findViewById(R.id.button_subscribe) _buttonSubscribe = findViewById(R.id.button_subscribe)
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings) _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
_overlayLoading = findViewById(R.id.channel_loading_overlay) _overlayLoading = findViewById(R.id.channel_loading_overlay)
_overlayLoadingSpinner = findViewById(R.id.channel_loader) _overlayLoadingSpinner = findViewById(R.id.channel_loader_frag)
_overlayContainer = findViewById(R.id.overlay_container) _overlayContainer = findViewById(R.id.overlay_container)
_buttonSubscribe.onSubscribed.subscribe { _buttonSubscribe.onSubscribed.subscribe {
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer) UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
@@ -226,6 +226,8 @@ class ChannelFragment : MainFragment() {
if (content is IPlatformVideo) { if (content is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
UIDialogs.toast("Added to watch later\n[${content.name}]") UIDialogs.toast("Added to watch later\n[${content.name}]")
else
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
} }
} }
adapter.onUrlClicked.subscribe { url -> adapter.onUrlClicked.subscribe { url ->
@@ -86,6 +86,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
if(it is IPlatformVideo) { if(it is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
UIDialogs.toast("Added to watch later\n[${it.name}]"); UIDialogs.toast("Added to watch later\n[${it.name}]");
else
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
} }
}; };
adapter.onLongPress.subscribe(this) { adapter.onLongPress.subscribe(this) {
@@ -101,7 +101,7 @@ class VideoDetailFragment() : MainFragment() {
} }
private fun isSmallWindow(): Boolean { private fun isSmallWindow(): Boolean {
return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.column_width_dp) * 2 return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.smallest_width_dp)
} }
private fun isAutoRotateEnabled(): Boolean { private fun isAutoRotateEnabled(): Boolean {
@@ -627,11 +627,6 @@ class VideoDetailFragment() : MainFragment() {
showSystemUI() showSystemUI()
} }
// temporarily force the device to portrait if auto-rotate is disabled to prevent landscape when exiting full screen on a small device
// @SuppressLint("SourceLockedOrientationActivity")
// if (!isFullscreen && isSmallWindow() && !isAutoRotateEnabled() && !isMinimizingFromFullScreen) {
// activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
// }
updateOrientation(); updateOrientation();
_view?.allowMotion = !fullscreen; _view?.allowMotion = !fullscreen;
} }
@@ -4,6 +4,7 @@ import android.app.PictureInPictureParams
import android.app.RemoteAction import android.app.RemoteAction
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
@@ -79,7 +80,9 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.casting.CastConnectionState import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
@@ -93,6 +96,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptAgeException
import com.futo.platformplayer.engine.exceptions.ScriptException import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlLinks
@@ -174,6 +178,7 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToLong import kotlin.math.roundToLong
@@ -607,6 +612,10 @@ class VideoDetailView : ConstraintLayout {
} }
} }
_player.onReloadRequired.subscribe {
fetchVideo();
}
_player.onPlayChanged.subscribe { _player.onPlayChanged.subscribe {
if (StateCasting.instance.activeDevice == null) { if (StateCasting.instance.activeDevice == null) {
handlePlayChanged(it); handlePlayChanged(it);
@@ -800,6 +809,8 @@ class VideoDetailView : ConstraintLayout {
_lastVideoSource = null; _lastVideoSource = null;
_lastAudioSource = null; _lastAudioSource = null;
_lastSubtitleSource = null; _lastSubtitleSource = null;
_cast.cancel()
StateCasting.instance.cancel()
video = null; video = null;
_container_content_liveChat?.close(); _container_content_liveChat?.close();
_player.clear(); _player.clear();
@@ -1892,11 +1903,46 @@ class VideoDetailView : ConstraintLayout {
} }
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) { private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)") Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)")
castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)
}
if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)) { private fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
_cast.setVideoDetails(video, resumePositionMs / 1000); fragment.lifecycleScope.launch(Dispatchers.IO) {
setCastEnabled(true); try {
} else throw IllegalStateException("Disconnected cast during loading"); val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin()
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin()
else null
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
try {
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
_cast.setLoading(it)
}, onLoadingEstimate = {
_cast.setLoading(it)
})
if (castingSucceeded) {
withContext(Dispatchers.Main) {
_cast.setVideoDetails(video, resumePositionMs / 1000);
setCastEnabled(true);
}
}
} catch (e: ScriptReloadRequiredException) {
Log.i(TAG, "Reload required exception", e)
if (plugin == null)
throw e
if (startId != -1 && plugin.getUnderlyingPlugin().runtimeId != startId)
throw e
StatePlatform.instance.handleReloadRequired(e, {
fetchVideo()
});
}
} catch (e: Throwable) {
Logger.e(TAG, "loadCurrentVideoCast", e)
}
}
} }
//Events //Events
@@ -1933,8 +1979,8 @@ class VideoDetailView : ConstraintLayout {
} }
updateQualityFormatsOverlay( updateQualityFormatsOverlay(
videoTrackFormats.distinctBy { it.height }.sortedBy { it.height }, videoTrackFormats.distinctBy { it.height }.sortedByDescending { it.height },
audioTrackFormats.distinctBy { it.bitrate }.sortedBy { it.bitrate }); audioTrackFormats.distinctBy { it.bitrate }.sortedByDescending { it.bitrate });
} }
} }
@@ -2192,19 +2238,19 @@ class VideoDetailView : ConstraintLayout {
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply { if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds(); val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
val format = if(playbackSpeeds.size < 20) "%.2f" else "%.1f"; val format = if(playbackSpeeds.size < 20) "%.2f" else "%.1f";
val playbackLabels = playbackSpeeds.map { String.format(format, it) }.toMutableList(); val playbackLabels = playbackSpeeds.map { String.format(Locale.US, format, it) }.toMutableList();
playbackLabels.add("+"); playbackLabels.add("+");
playbackLabels.add(0, "-"); playbackLabels.add(0, "-");
setButtons(playbackLabels, String.format(format, currentPlaybackRate)); setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate));
onClick.subscribe { v -> onClick.subscribe { v ->
val currentPlaybackSpeed = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate(); val currentPlaybackSpeed = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate();
var playbackSpeedString = v; var playbackSpeedString = v;
val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep(); val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
if(v == "+") if(v == "+")
playbackSpeedString = String.format("%.2f", Math.min((currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed, 5.0)).toString(); playbackSpeedString = String.format(Locale.US, "%.2f", Math.min((currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed, 5.0)).toString();
else if(v == "-") else if(v == "-")
playbackSpeedString = String.format("%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString(); playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString();
val newPlaybackSpeed = playbackSpeedString.toDouble(); val newPlaybackSpeed = playbackSpeedString.toDouble();
if (_isCasting) { if (_isCasting) {
val ad = StateCasting.instance.activeDevice ?: return@subscribe val ad = StateCasting.instance.activeDevice ?: return@subscribe
@@ -2212,11 +2258,11 @@ class VideoDetailView : ConstraintLayout {
return@subscribe return@subscribe
} }
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", newPlaybackSpeed)})"); qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
ad.changeSpeed(newPlaybackSpeed) ad.changeSpeed(newPlaybackSpeed)
setSelected(playbackSpeedString); setSelected(playbackSpeedString);
} else { } else {
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", newPlaybackSpeed)})"); qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
_player.setPlaybackRate(playbackSpeedString.toFloat()); _player.setPlaybackRate(playbackSpeedString.toFloat());
setSelected(playbackSpeedString); setSelected(playbackSpeedString);
} }
@@ -2409,7 +2455,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true)) else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player? _player.hideControls(false); //TODO: Disable player?
@@ -2424,7 +2470,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed)
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true)) else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player? _player.hideControls(false); //TODO: Disable player?
@@ -2440,7 +2486,7 @@ class VideoDetailView : ConstraintLayout {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else else
_player.swapSubtitles(fragment.lifecycleScope, toSet); _player.swapSubtitles(fragment.lifecycleScope, toSet);
@@ -2491,7 +2537,9 @@ class VideoDetailView : ConstraintLayout {
val url = _url; val url = _url;
if (!url.isNullOrBlank()) { if (!url.isNullOrBlank()) {
setLoading(true); fragment.lifecycleScope.launch(Dispatchers.Main) {
setLoading(true);
}
_taskLoadVideo.run(url); _taskLoadVideo.run(url);
} }
} }
@@ -2545,8 +2593,7 @@ class VideoDetailView : ConstraintLayout {
_cast.visibility = View.VISIBLE; _cast.visibility = View.VISIBLE;
} else { } else {
StateCasting.instance.stopVideo(); StateCasting.instance.stopVideo();
_cast.stopTimeJob(); _cast.cancel()
_cast.visibility = View.GONE;
if (video?.isLive == false) { if (video?.isLive == false) {
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed()); _player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
@@ -2757,6 +2804,8 @@ class VideoDetailView : ConstraintLayout {
if(it is IPlatformVideo) { if(it is IPlatformVideo) {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
UIDialogs.toast("Added to watch later\n[${it.name}]"); UIDialogs.toast("Added to watch later\n[${it.name}]");
else
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
} }
} }
onAddToQueueClicked.subscribe(this) { onAddToQueueClicked.subscribe(this) {
@@ -3024,6 +3073,11 @@ class VideoDetailView : ConstraintLayout {
return@TaskHandler result; return@TaskHandler result;
}) })
.success { setVideoDetails(it, true) } .success { setVideoDetails(it, true) }
.exception<ScriptReloadRequiredException> {
StatePlatform.instance.handleReloadRequired(it, {
fetchVideo();
});
}
.exception<NoPlatformClientException> { .exception<NoPlatformClientException> {
Logger.w(TAG, "exception<NoPlatformClientException>", it) Logger.w(TAG, "exception<NoPlatformClientException>", it)
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -43,6 +44,7 @@ class Playlist {
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? { fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? {
if(obj == null) if(obj == null)
return null; return null;
obj.ensureIsBusy();
val contextName = "Playlist"; val contextName = "Playlist";
@@ -62,7 +62,7 @@ class DownloadService : Service() {
Logger.i(TAG, "onStartCommand"); Logger.i(TAG, "onStartCommand");
synchronized(this) { synchronized(this) {
if(_started) if(_started)
return START_STICKY; return START_NOT_STICKY;
if(!FragmentedStorage.isInitialized) { if(!FragmentedStorage.isInitialized) {
Logger.i(TAG, "Attempted to start DownloadService without initialized files"); Logger.i(TAG, "Attempted to start DownloadService without initialized files");
@@ -2,6 +2,7 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import androidx.collection.LruCache import androidx.collection.LruCache
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
@@ -38,6 +39,7 @@ import com.futo.platformplayer.awaitFirstNotNullDeferred
import com.futo.platformplayer.constructs.BatchedTaskHandler import com.futo.platformplayer.constructs.BatchedTaskHandler
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.fromPool import com.futo.platformplayer.fromPool
import com.futo.platformplayer.getNowDiffDays import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.getNowDiffSeconds import com.futo.platformplayer.getNowDiffSeconds
@@ -316,7 +318,18 @@ class StatePlatform {
_platformOrderPersistent.save(); _platformOrderPersistent.save();
} }
suspend fun reloadClient(context: Context, id: String) : JSClient? { fun handleReloadRequired(reloadRequiredException: ScriptReloadRequiredException, afterReload: (() -> Unit)? = null) {
val id = if(reloadRequiredException.config is SourcePluginConfig) reloadRequiredException.config.id else "";
UIDialogs.appToast("Reloading [${reloadRequiredException.config.name}] by plugin request");
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
if(!reloadRequiredException.reloadData.isNullOrEmpty())
reEnableClientWithData(id, reloadRequiredException.reloadData, afterReload);
else
reEnableClient(id, afterReload);
}
}
suspend fun reloadClient(context: Context, id: String, afterReload: (()->Unit)? = null) : JSClient? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val client = getClient(id); val client = getClient(id);
if (client !is JSClient) if (client !is JSClient)
@@ -347,10 +360,27 @@ class StatePlatform {
_availableClients.removeIf { it.id == id }; _availableClients.removeIf { it.id == id };
_availableClients.add(newClient); _availableClients.add(newClient);
} }
afterReload?.invoke();
return@withContext newClient; return@withContext newClient;
}; };
} }
suspend fun reEnableClientWithData(id: String, data: String? = null, afterReload: (()->Unit)? = null) {
val enabledBefore = getEnabledClients().map { it.id };
if(data != null) {
val client = getClientOrNull(id);
if(client != null && client is JSClient)
client.setReloadData(data);
}
selectClients({
_scope.launch(Dispatchers.IO) {
selectClients({
afterReload?.invoke();
}, *(enabledBefore).distinct().toTypedArray());
}
}, *(enabledBefore.filter { it != id }).distinct().toTypedArray())
}
suspend fun reEnableClient(id: String, afterReload: (()->Unit)? = null) = reEnableClientWithData(id, null, afterReload);
suspend fun enableClient(ids: List<String>) { suspend fun enableClient(ids: List<String>) {
val currentClients = getEnabledClients().map { it.id }; val currentClients = getEnabledClients().map { it.id };
@@ -361,9 +391,13 @@ class StatePlatform {
* If a client is disabled, NO requests are made to said client * If a client is disabled, NO requests are made to said client
*/ */
suspend fun selectClients(vararg ids: String) { suspend fun selectClients(vararg ids: String) {
selectClients(null, *ids);
}
suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var removed: MutableList<IPlatformClient>;
synchronized(_clientsLock) { synchronized(_clientsLock) {
val removed = _enabledClients.toMutableList(); removed = _enabledClients.toMutableList();
_enabledClients.clear(); _enabledClients.clear();
for (id in ids) { for (id in ids) {
val client = getClient(id); val client = getClient(id);
@@ -379,12 +413,13 @@ class StatePlatform {
} }
_enabledClientsPersistent.set(*ids); _enabledClientsPersistent.set(*ids);
_enabledClientsPersistent.save(); _enabledClientsPersistent.save();
for (oldClient in removed) {
oldClient.disable();
onSourceDisabled.emit(oldClient);
}
} }
for (oldClient in removed) {
oldClient.disable();
onSourceDisabled.emit(oldClient);
}
afterLoad?.invoke();
}; };
} }
@@ -935,7 +970,7 @@ class StatePlatform {
return EmptyPager(); return EmptyPager();
if(!StateApp.instance.privateMode) if(!StateApp.instance.privateMode)
return client.fromPool(_mainClientPool).getComments(url); return client.fromPool(_pagerClientPool).getComments(url);
else else
return client.fromPool(_privateClientPool).getComments(url); return client.fromPool(_privateClientPool).getComments(url);
} }
@@ -3,7 +3,6 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
@@ -21,7 +20,6 @@ import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.sToOffsetDateTimeUTC import com.futo.platformplayer.sToOffsetDateTimeUTC
import com.futo.platformplayer.smartMerge import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringDateMapStorage import com.futo.platformplayer.stores.StringDateMapStorage
@@ -30,15 +28,12 @@ import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.ReconstructStore import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.sync.internal.GJSyncOpcodes import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
import com.futo.platformplayer.sync.models.SyncWatchLaterPackage import com.futo.platformplayer.sync.models.SyncWatchLaterPackage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -178,31 +173,30 @@ class StatePlaylists {
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER); StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
} }
} }
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean { fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false): Boolean {
var wasNew = false;
synchronized(_watchlistStore) { synchronized(_watchlistStore) {
if(!_watchlistStore.hasItem { it.url == video.url }) if (_watchlistStore.hasItem { it.url == video.url }) {
wasNew = true; return false
_watchlistStore.saveAsync(video);
if(orderPosition == -1)
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
else {
val existing = _watchlistOrderStore.getAllValues().toMutableList();
existing.add(orderPosition, video.url);
_watchlistOrderStore.set(*existing.toTypedArray());
} }
_watchlistOrderStore.save();
_watchlistStore.saveAsync(video)
if (Settings.instance.other.watchLaterAddStart) {
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray())
} else {
_watchlistOrderStore.set(*(_watchlistOrderStore.values + listOf(video.url)).toTypedArray())
}
_watchlistOrderStore.save()
} }
onWatchLaterChanged.emit(); onWatchLaterChanged.emit();
if(isUserInteraction) { if (isUserInteraction) {
val now = OffsetDateTime.now(); val now = OffsetDateTime.now();
_watchLaterAdds.setAndSave(video.url, now); _watchLaterAdds.setAndSave(video.url, now);
broadcastWatchLaterAddition(video, now); broadcastWatchLaterAddition(video, now);
} }
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
return wasNew; return true;
} }
fun getLastPlayedPlaylist() : Playlist? { fun getLastPlayedPlaylist() : Playlist? {
@@ -78,7 +78,13 @@ class StateSync {
onAuthorized = { sess, isNewlyAuthorized, isNewSession -> onAuthorized = { sess, isNewlyAuthorized, isNewSession ->
if (isNewSession) { if (isNewSession) {
deviceUpdatedOrAdded.emit(sess.remotePublicKey, sess) 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)
}
}
} }
} }
@@ -174,7 +174,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
if (resolve != null) { if (resolve != null) {
resolveCount = resolves.size; resolveCount = resolves.size;
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}") UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})")
for(result in resolve){ for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl }; val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) { if(task != null) {
@@ -0,0 +1,381 @@
package com.futo.platformplayer.views
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.OvershootInterpolator
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.toColorInt
import kotlin.math.*
import kotlin.random.Random
import com.futo.platformplayer.UIDialogs
class TargetTapLoaderView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val primaryColor = "#2D63ED".toColorInt()
private val inactiveGlobalAlpha = 110
private val idleSpeedMultiplier = .015f
private val overshootInterpolator = OvershootInterpolator(1.5f)
private val floatAccel = .03f
private val idleMaxSpeed = .35f
private val idleInitialTargets = 10
private val idleHintText = "Waiting for media to become available"
private var expectedDurationMs: Long? = null
private var loadStartTime = 0L
private var playStartTime = 0L
private var loaderFinished = false
private var forceIndeterminate= false
private var lastFrameTime = System.currentTimeMillis()
private var score = 0
private var isPlaying = false
private val targets = mutableListOf<Target>()
private val particles = mutableListOf<Particle>()
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(0.7f, 1f, 1f, 1f)
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics)
textAlign = Paint.Align.LEFT
setShadowLayer(4f, 0f, 0f, Color.BLACK)
typeface = Typeface.DEFAULT_BOLD
}
private val idleHintPaint = Paint(textPaint).apply {
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics)
typeface = Typeface.DEFAULT
setShadowLayer(2f, 0f, 0f, Color.BLACK)
}
private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = primaryColor }
private val spinnerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = primaryColor; strokeWidth = 12f
style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND
}
private val outerRingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val middleRingPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val centerDotPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.argb(50, 0, 0, 0) }
private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val particlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val backgroundPaint = Paint()
private var spinnerShader: SweepGradient? = null
private var spinnerAngle = 0f
private val MIN_SPAWN_RATE = 1f
private val MAX_SPAWN_RATE = 20.0f
private val HIT_RATE_INCREMENT = 0.15f
private val MISS_RATE_DECREMENT = 0.09f
private var spawnRate = MIN_SPAWN_RATE
private val frameRunnable = object : Runnable {
override fun run() { invalidate(); if (!loaderFinished) postDelayed(this, 16L) }
}
init { setOnTouchListener { _, e -> if (e.action == MotionEvent.ACTION_DOWN) handleTap(e.x, e.y); true } }
fun startLoader(durationMs: Long? = null) {
val alreadyRunning = !loaderFinished
if (alreadyRunning && durationMs == null) {
expectedDurationMs = null
forceIndeterminate = true
return
}
expectedDurationMs = durationMs?.takeIf { it > 0 }
forceIndeterminate = expectedDurationMs == null
loaderFinished = false
isPlaying = false
score = 0
particles.clear()
spawnRate = MIN_SPAWN_RATE
post { if (targets.isEmpty()) prepopulateIdleTargets() }
loadStartTime = System.currentTimeMillis()
playStartTime = 0
removeCallbacks(frameRunnable)
post(frameRunnable)
if (!isIndeterminate) {
postDelayed({
if (!loaderFinished) {
forceIndeterminate = true
expectedDurationMs = null
}
}, expectedDurationMs!!)
}
}
fun finishLoader() {
loaderFinished = true
particles.clear()
isPlaying = false
invalidate()
}
fun stopAndResetLoader() {
if (score > 0) {
val elapsed = (System.currentTimeMillis() - (if (playStartTime > 0) playStartTime else loadStartTime)) / 1000.0
UIDialogs.toast("Nice! score $score | ${"%.1f".format(score / elapsed)} / s")
score = 0
}
loaderFinished = true
isPlaying = false
targets.clear()
particles.clear()
removeCallbacks(frameRunnable)
invalidate()
}
private val isIndeterminate get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L
private fun handleTap(x: Float, y: Float) {
val idx = targets.indexOfFirst { !it.hit && hypot(x - it.x, y - it.y) <= it.radius }
if (idx >= 0) {
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
val t = targets[idx]
t.hit = true; t.hitTime = System.currentTimeMillis()
accelerateSpawnRate()
score += if (!isIndeterminate) 10 else 5
spawnParticles(t.x, t.y, t.radius)
if (!isPlaying) {
isPlaying = true
playStartTime = System.currentTimeMillis()
score = 0
spawnRate = MIN_SPAWN_RATE
targets.retainAll { it === t }
spawnTarget()
}
} else if (isPlaying) decelerateSpawnRate()
}
private inline fun accelerateSpawnRate() {
spawnRate = (spawnRate + HIT_RATE_INCREMENT).coerceAtMost(MAX_SPAWN_RATE)
}
private inline fun decelerateSpawnRate() {
spawnRate = (spawnRate - MISS_RATE_DECREMENT).coerceAtLeast(MIN_SPAWN_RATE)
}
private fun spawnTarget() {
if (loaderFinished || width == 0 || height == 0) {
postDelayed({ spawnTarget() }, 200L); return
}
if (!isPlaying) {
postDelayed({ spawnTarget() }, 500L); return
}
val radius = Random.nextInt(40, 80).toFloat()
val x = Random.nextFloat() * (width - 2 * radius) + radius
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
val baseSpeed = Random.nextFloat() + .1f
val speed = baseSpeed
val angle = Random.nextFloat() * TAU
val vx = cos(angle) * speed
val vy = sin(angle) * speed
val alpha = Random.nextInt(150, 255)
targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy)
val delay = (1000f / spawnRate).roundToLong()
postDelayed({ spawnTarget() }, delay)
}
private fun prepopulateIdleTargets() {
if (width == 0 || height == 0) {
post { prepopulateIdleTargets() }
return
}
repeat(idleInitialTargets) {
val radius = Random.nextInt(40, 80).toFloat()
val x = Random.nextFloat() * (width - 2 * radius) + radius
val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius
val angle = Random.nextFloat() * TAU
val speed = (Random.nextFloat() * .3f + .05f) * idleSpeedMultiplier
val vx = cos(angle) * speed
val vy = sin(angle) * speed
val alpha = Random.nextInt(60, 110)
targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy)
}
}
private fun spawnParticles(cx: Float, cy: Float, radius: Float) {
repeat(12) {
val angle = Random.nextFloat() * TAU
val speed = Random.nextFloat() * 5f + 2f
val vx = cos(angle) * speed
val vy = sin(angle) * speed
val col = ColorUtils.setAlphaComponent(primaryColor, Random.nextInt(120, 255))
particles += Particle(cx, cy, vx, vy, System.currentTimeMillis(), col)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val now = System.currentTimeMillis()
val deltaMs = now - lastFrameTime
lastFrameTime = now
drawBackground(canvas)
drawTargets(canvas, now)
drawParticles(canvas, now)
if (!loaderFinished) {
if (isIndeterminate) drawIndeterminateSpinner(canvas, deltaMs)
else drawDeterministicProgressBar(canvas, now)
}
if (isPlaying) {
val margin = 24f
val scoreTxt = "Score: $score"
val speedTxt = "Speed: ${"%.2f".format(spawnRate)}/s"
val maxWidth = width - margin
val needRight = max(textPaint.measureText(scoreTxt), textPaint.measureText(speedTxt)) > maxWidth
val alignX = if (needRight) (width - margin) else margin
textPaint.textAlign = if (needRight) Paint.Align.RIGHT else Paint.Align.LEFT
canvas.drawText(scoreTxt, alignX, textPaint.textSize + margin, textPaint)
canvas.drawText(speedTxt, alignX, 2*textPaint.textSize + margin + 4f, textPaint)
textPaint.textAlign = Paint.Align.LEFT
}
else if (loaderFinished)
canvas.drawText("Loading Complete!", width/2f, height/2f, textPaint.apply { textAlign = Paint.Align.CENTER })
else {
idleHintPaint.textAlign = Paint.Align.CENTER
canvas.drawText(idleHintText, width / 2f, height - 48f, idleHintPaint)
}
}
private fun drawBackground(canvas: Canvas) {
val colors = intArrayOf(
Color.rgb(20, 20, 40),
Color.rgb(15, 15, 30),
Color.rgb(10, 10, 20),
Color.rgb( 5, 5, 10),
Color.BLACK
)
val pos = floatArrayOf(0f, 0.25f, 0.5f, 0.75f, 1f)
if (backgroundPaint.shader == null) {
backgroundPaint.shader = LinearGradient(
0f, 0f, 0f, height.toFloat(),
colors, pos,
Shader.TileMode.CLAMP
)
}
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint)
}
private fun drawTargets(canvas: Canvas, now: Long) {
val expireMsActive = if (isIndeterminate) 2500L else 1500L
val it = targets.iterator()
while (it.hasNext()) {
val t = it.next()
if (t.hit && now - t.hitTime > 300L) { it.remove(); continue }
if (isPlaying && !t.hit && now - t.spawnTime > expireMsActive) {
it.remove(); decelerateSpawnRate(); continue
}
t.x += t.vx; t.y += t.vy
t.vx += (Random.nextFloat() - .5f) * floatAccel
t.vy += (Random.nextFloat() - .5f) * floatAccel
val speedCap = if (isPlaying) Float.MAX_VALUE else idleMaxSpeed
val mag = hypot(t.vx, t.vy)
if (mag > speedCap) {
val s = speedCap / mag
t.vx *= s; t.vy *= s
}
if (t.x - t.radius < 0 || t.x + t.radius > width) t.vx *= -1
if (t.y - t.radius < 0 || t.y + t.radius > height) t.vy *= -1
val scale = if (t.hit) 1f - ((now - t.hitTime) / 300f).coerceIn(0f,1f)
else {
val e = now - t.spawnAnimStart
if (e < 300L) overshootInterpolator.getInterpolation(e/300f)
else 1f + .02f * sin(((now - t.spawnAnimStart)/1000f)*TAU + t.pulseOffset)
}
val animAlpha = if (t.hit) ((1f - scale)*255).toInt() else 255
val globalAlpha = if (isPlaying) 255 else inactiveGlobalAlpha
val alpha = (animAlpha * t.baseAlpha /255f * globalAlpha/255f).toInt().coerceAtMost(255)
val r = max(1f, t.radius*scale)
val outerCol = ColorUtils.setAlphaComponent(primaryColor, alpha)
val midCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.7f).toInt())
val innerCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.4f).toInt())
outerRingPaint.color = outerCol; middleRingPaint.color = midCol; centerDotPaint.color = innerCol
glowPaint.shader = RadialGradient(t.x, t.y, r, outerCol, Color.TRANSPARENT, Shader.TileMode.CLAMP)
canvas.drawCircle(t.x, t.y, r*1.2f, glowPaint)
canvas.drawCircle(t.x+4f, t.y+4f, r, shadowPaint)
canvas.drawCircle(t.x, t.y, r, outerRingPaint)
canvas.drawCircle(t.x, t.y, r*.66f, middleRingPaint)
canvas.drawCircle(t.x, t.y, r*.33f, centerDotPaint)
}
}
private fun drawParticles(canvas: Canvas, now: Long) {
val lifespan = 400L
val it = particles.iterator()
while (it.hasNext()) {
val p = it.next()
val age = now - p.startTime
if (age > lifespan) { it.remove(); continue }
val a = ((1f - age/lifespan.toFloat())*255).toInt()
particlePaint.color = ColorUtils.setAlphaComponent(p.baseColor, a)
p.x += p.vx; p.y += p.vy
canvas.drawCircle(p.x, p.y, 6f, particlePaint)
}
}
private fun drawDeterministicProgressBar(canvas: Canvas, now: Long) {
val dur = expectedDurationMs ?: return
val prog = ((now - loadStartTime) / dur.toFloat()).coerceIn(0f, 1f)
val eased = AccelerateDecelerateInterpolator().getInterpolation(prog)
val h = 20f; val r=10f
canvas.drawRoundRect(RectF(0f, height-h, width*eased, height.toFloat()), r, r, progressBarPaint)
}
private fun drawIndeterminateSpinner(canvas: Canvas, dt: Long) {
val cx=width/2f; val cy=height/2f; val r=min(width,height)/6f
spinnerAngle = (spinnerAngle + .25f*dt)%360f
if(spinnerShader == null) spinnerShader = SweepGradient(cx,cy,intArrayOf(Color.TRANSPARENT,Color.WHITE,Color.TRANSPARENT),floatArrayOf(0f,.5f,1f))
spinnerPaint.shader = spinnerShader
val glow = Paint(spinnerPaint).apply{ maskFilter = BlurMaskFilter(15f,BlurMaskFilter.Blur.SOLID) }
val sweep = 270f
canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,glow)
canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,spinnerPaint)
}
private data class Target(
var x: Float,
var y: Float,
val radius: Float,
val spawnTime: Long,
var hit: Boolean = false,
var hitTime: Long = 0L,
val baseAlpha: Int = 255,
var vx: Float=0f,
var vy:Float=0f,
val spawnAnimStart: Long = System.currentTimeMillis(),
val pulseOffset: Float = Random.nextFloat() * TAU
)
private data class Particle(
var x:Float,
var y:Float,
val vx:Float,
val vy:Float,
val startTime:Long,
val baseColor:Int
)
private companion object { private const val TAU = (2 * Math.PI).toFloat() }
}
@@ -43,7 +43,6 @@ class HistoryListViewHolder : ViewHolder {
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) { constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) {
_root = itemView.findViewById(R.id.root); _root = itemView.findViewById(R.id.root);
_imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail); _imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail);
_imageThumbnail.clipToOutline = true;
_textName = itemView.findViewById(R.id.text_video_name); _textName = itemView.findViewById(R.id.text_video_name);
_textAuthor = itemView.findViewById(R.id.text_author); _textAuthor = itemView.findViewById(R.id.text_author);
_textMetadata = itemView.findViewById(R.id.text_video_metadata); _textMetadata = itemView.findViewById(R.id.text_video_metadata);
@@ -51,7 +51,6 @@ class VideoListEditorViewHolder : ViewHolder {
constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) { constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) {
_root = view.findViewById(R.id.root); _root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail); _imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true;
_textName = view.findViewById(R.id.text_video_name); _textName = view.findViewById(R.id.text_video_name);
_textAuthor = view.findViewById(R.id.text_author); _textAuthor = view.findViewById(R.id.text_author);
_textMetadata = view.findViewById(R.id.text_video_metadata); _textMetadata = view.findViewById(R.id.text_video_metadata);
@@ -95,7 +94,13 @@ class VideoListEditorViewHolder : ViewHolder {
.into(_imageThumbnail); .into(_imageThumbnail);
_textName.text = v.name; _textName.text = v.name;
_textAuthor.text = v.author.name; _textAuthor.text = v.author.name;
_textVideoDuration.text = v.duration.toHumanTime(false);
if(v.duration > 0) {
_textVideoDuration.text = v.duration.toHumanTime(false);
_textVideoDuration.visibility = View.VISIBLE;
}
else
_textVideoDuration.visibility = View.GONE;
val historyPosition = StateHistory.instance.getHistoryPosition(v.url) val historyPosition = StateHistory.instance.getHistoryPosition(v.url)
_timeBar.progress = historyPosition.toFloat() / v.duration.toFloat(); _timeBar.progress = historyPosition.toFloat() / v.duration.toFloat();
@@ -29,7 +29,6 @@ class VideoListHorizontalViewHolder : ViewHolder {
constructor(view: View) : super(view) { constructor(view: View) : super(view) {
_root = view.findViewById(R.id.root); _root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail); _imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true;
_textName = view.findViewById(R.id.text_video_name); _textName = view.findViewById(R.id.text_video_name);
_textAuthor = view.findViewById(R.id.text_author); _textAuthor = view.findViewById(R.id.text_author);
_textVideoDuration = view.findViewById(R.id.thumbnail_duration); _textVideoDuration = view.findViewById(R.id.thumbnail_duration);
@@ -204,8 +204,14 @@ open class PreviewVideoView : LinearLayout {
.into(_imageVideo); .into(_imageVideo);
}; };
if(!isPlanned) if(!isPlanned) {
_textVideoDuration.text = video.duration.toHumanTime(false); if(video.duration > 0) {
_textVideoDuration.text = video.duration.toHumanTime(false);
_textVideoDuration.visibility = View.VISIBLE;
}
else
_textVideoDuration.visibility = View.GONE;
}
else else
_textVideoDuration.text = context.getString(R.string.planned); _textVideoDuration.text = context.getString(R.string.planned);
@@ -39,6 +39,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
class GestureControlView : LinearLayout { class GestureControlView : LinearLayout {
@@ -79,6 +82,9 @@ class GestureControlView : LinearLayout {
private var _adjustingFullscreenDown: Boolean = false; private var _adjustingFullscreenDown: Boolean = false;
private var _fullScreenFactorUp = 1.0f; private var _fullScreenFactorUp = 1.0f;
private var _fullScreenFactorDown = 1.0f; private var _fullScreenFactorDown = 1.0f;
private val _layoutHoldSpeed: LinearLayout
private val _textHoldFastForward: TextView
private val _imageHoldFastForward: ImageView
private var _scaleGestureDetector: ScaleGestureDetector private var _scaleGestureDetector: ScaleGestureDetector
private var _scaleFactor = 1.0f private var _scaleFactor = 1.0f
@@ -92,6 +98,11 @@ class GestureControlView : LinearLayout {
private var _surfaceView: View? = null private var _surfaceView: View? = null
private var _layoutIndicatorFill: FrameLayout; private var _layoutIndicatorFill: FrameLayout;
private var _layoutIndicatorFit: FrameLayout; private var _layoutIndicatorFit: FrameLayout;
private var _speedHolding = false
private val _speedFormatter = DecimalFormat("#.##", DecimalFormatSymbols(Locale.US)).apply {
roundingMode = java.math.RoundingMode.HALF_UP
}
private val _gestureController: GestureDetectorCompat; private val _gestureController: GestureDetectorCompat;
@@ -103,6 +114,8 @@ class GestureControlView : LinearLayout {
val onZoom = Event1<Float>(); val onZoom = Event1<Float>();
val onSoundAdjusted = Event1<Float>(); val onSoundAdjusted = Event1<Float>();
val onToggleFullscreen = Event0(); val onToggleFullscreen = Event0();
val onSpeedHoldStart = Event0()
val onSpeedHoldEnd = Event0()
var fullScreenGestureEnabled = true var fullScreenGestureEnabled = true
@@ -124,6 +137,9 @@ class GestureControlView : LinearLayout {
_layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen); _layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
_layoutIndicatorFill = findViewById(R.id.layout_indicator_fill); _layoutIndicatorFill = findViewById(R.id.layout_indicator_fill);
_layoutIndicatorFit = findViewById(R.id.layout_indicator_fit); _layoutIndicatorFit = findViewById(R.id.layout_indicator_fit);
_layoutHoldSpeed = findViewById(R.id.layout_controls_increased_speed)
_textHoldFastForward = findViewById(R.id.text_holdFastForward)
_imageHoldFastForward = findViewById(R.id.image_holdFastForward)
_scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { _scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean { override fun onScale(detector: ScaleGestureDetector): Boolean {
@@ -216,7 +232,21 @@ class GestureControlView : LinearLayout {
return true; return true;
} }
override fun onLongPress(p0: MotionEvent) = Unit override fun onLongPress(p0: MotionEvent) {
if (!_isControlsLocked
&& !_skipping
&& !_adjustingBrightness
&& !_adjustingSound
&& !_adjustingFullscreenUp
&& !_adjustingFullscreenDown
&& !_isPanning
&& !_isZooming
&& Settings.instance.playback.getHoldPlaybackSpeed() > 1.0) {
_speedHolding = true
showHoldSpeedControls()
onSpeedHoldStart.emit()
}
}
}); });
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener { _gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
@@ -301,6 +331,17 @@ class GestureControlView : LinearLayout {
onPan.emit(_translationX, _translationY) onPan.emit(_translationX, _translationY)
} }
private fun showHoldSpeedControls() {
_layoutHoldSpeed.visibility = View.VISIBLE
_textHoldFastForward.text = _speedFormatter.format(Settings.instance.playback.getHoldPlaybackSpeed()) + "x"
(_imageHoldFastForward.drawable as? Animatable)?.start()
}
private fun hideHoldSpeedControls() {
_layoutHoldSpeed.visibility = View.GONE
(_imageHoldFastForward.drawable as? Animatable)?.stop()
}
fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) { fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) {
_layoutControls = layoutControls; _layoutControls = layoutControls;
_background = background; _background = background;
@@ -309,6 +350,12 @@ class GestureControlView : LinearLayout {
override fun onTouchEvent(event: MotionEvent?): Boolean { override fun onTouchEvent(event: MotionEvent?): Boolean {
val ev = event ?: return super.onTouchEvent(event); val ev = event ?: return super.onTouchEvent(event);
if (ev.action == MotionEvent.ACTION_UP && _speedHolding) {
_speedHolding = false
hideHoldSpeedControls()
onSpeedHoldEnd.emit()
}
cancelHideJob(); cancelHideJob();
if (_skipping) { if (_skipping) {
@@ -18,6 +18,7 @@ import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.TimeBar import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.casting.AirPlayCastingDevice import com.futo.platformplayer.casting.AirPlayCastingDevice
@@ -29,6 +30,7 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.formatDuration import com.futo.platformplayer.formatDuration
import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -53,11 +55,14 @@ class CastView : ConstraintLayout {
private val _timeBar: DefaultTimeBar; private val _timeBar: DefaultTimeBar;
private val _background: FrameLayout; private val _background: FrameLayout;
private val _gestureControlView: GestureControlView; private val _gestureControlView: GestureControlView;
private val _loaderGame: TargetTapLoaderView
private var _scope: CoroutineScope = CoroutineScope(Dispatchers.Main); private var _scope: CoroutineScope = CoroutineScope(Dispatchers.Main);
private var _updateTimeJob: Job? = null; private var _updateTimeJob: Job? = null;
private var _inPictureInPicture: Boolean = false; private var _inPictureInPicture: Boolean = false;
private var _chapters: List<IChapter>? = null; private var _chapters: List<IChapter>? = null;
private var _currentChapter: IChapter? = null; private var _currentChapter: IChapter? = null;
private var _speedHoldPrevRate = 1.0
private var _speedHoldWasPlaying = false
val onChapterChanged = Event2<IChapter?, Boolean>(); val onChapterChanged = Event2<IChapter?, Boolean>();
val onMinimizeClick = Event0(); val onMinimizeClick = Event0();
@@ -85,8 +90,25 @@ class CastView : ConstraintLayout {
_timeBar = findViewById(R.id.time_progress); _timeBar = findViewById(R.id.time_progress);
_background = findViewById(R.id.layout_background); _background = findViewById(R.id.layout_background);
_gestureControlView = findViewById(R.id.gesture_control); _gestureControlView = findViewById(R.id.gesture_control);
_loaderGame = findViewById(R.id.loader_overlay)
_loaderGame.visibility = View.GONE
_gestureControlView.fullScreenGestureEnabled = false _gestureControlView.fullScreenGestureEnabled = false
_gestureControlView.setupTouchArea(); _gestureControlView.setupTouchArea();
_gestureControlView.onSpeedHoldStart.subscribe {
val d = StateCasting.instance.activeDevice ?: return@subscribe;
_speedHoldWasPlaying = d.isPlaying
_speedHoldPrevRate = d.speed
if (d.canSetSpeed)
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
d.resumeVideo()
}
_gestureControlView.onSpeedHoldEnd.subscribe {
val d = StateCasting.instance.activeDevice ?: return@subscribe;
if (!_speedHoldWasPlaying) d.pauseVideo()
d.changeSpeed(_speedHoldPrevRate)
}
_gestureControlView.onSeek.subscribe { _gestureControlView.onSeek.subscribe {
val d = StateCasting.instance.activeDevice ?: return@subscribe; val d = StateCasting.instance.activeDevice ?: return@subscribe;
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000); StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
@@ -180,6 +202,12 @@ class CastView : ConstraintLayout {
_updateTimeJob = null; _updateTimeJob = null;
} }
fun cancel() {
stopTimeJob()
setLoading(false)
visibility = View.GONE
}
fun stopAllGestures() { fun stopAllGestures() {
_gestureControlView.stopAllGestures(); _gestureControlView.stopAllGestures();
} }
@@ -262,6 +290,7 @@ class CastView : ConstraintLayout {
_textDuration.text = (video.duration * 1000).formatDuration(); _textDuration.text = (video.duration * 1000).formatDuration();
_timeBar.setPosition(position); _timeBar.setPosition(position);
_timeBar.setDuration(video.duration); _timeBar.setDuration(video.duration);
setLoading(false)
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@@ -278,6 +307,7 @@ class CastView : ConstraintLayout {
_updateTimeJob?.cancel(); _updateTimeJob?.cancel();
_updateTimeJob = null; _updateTimeJob = null;
_scope.cancel(); _scope.cancel();
setLoading(false)
} }
private fun getPlaybackStateCompat(): Int { private fun getPlaybackStateCompat(): Int {
@@ -288,4 +318,19 @@ class CastView : ConstraintLayout {
else -> PlaybackStateCompat.STATE_PAUSED; else -> PlaybackStateCompat.STATE_PAUSED;
} }
} }
fun setLoading(isLoading: Boolean) {
if (isLoading) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader()
} else {
_loaderGame.visibility = View.GONE
_loaderGame.stopAndResetLoader()
}
}
fun setLoading(expectedDurationMs: Int) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader(expectedDurationMs.toLong())
}
} }
@@ -1,5 +1,6 @@
package com.futo.platformplayer.views.livechat package com.futo.platformplayer.views.livechat
import CSSColor
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.LevelListDrawable import android.graphics.drawable.LevelListDrawable
import android.text.Spannable import android.text.Spannable
@@ -24,6 +25,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.overlays.LiveChatOverlay import com.futo.platformplayer.views.overlays.LiveChatOverlay
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import toAndroidColor
class LiveChatDonationListItem(viewGroup: ViewGroup) class LiveChatDonationListItem(viewGroup: ViewGroup)
: LiveChatListItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chat_donation, viewGroup, false)) { : 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(); _amount.text = event.amount.trim();
if(event.colorDonation != null && event.colorDonation.isHexColor()) { if(event.colorDonation != null && event.colorDonation.isHexColor()) {
val color = Color.parseColor(event.colorDonation); val color = CSSColor.parseColor(event.colorDonation);
_amountContainer.background.setTint(color); _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); _amount.setTextColor(Color.BLACK);
else else
_amount.setTextColor(Color.WHITE); _amount.setTextColor(Color.WHITE);
@@ -13,6 +13,7 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.live.LiveEventDonation import com.futo.platformplayer.api.media.models.live.LiveEventDonation
import com.futo.platformplayer.isHexColor import com.futo.platformplayer.isHexColor
import toAndroidColor
class LiveChatDonationPill: LinearLayout { class LiveChatDonationPill: LinearLayout {
private val _imageAuthor: ImageView; private val _imageAuthor: ImageView;
@@ -33,10 +34,10 @@ class LiveChatDonationPill: LinearLayout {
if(donation.colorDonation != null && donation.colorDonation.isHexColor()) { if(donation.colorDonation != null && donation.colorDonation.isHexColor()) {
val color = Color.parseColor(donation.colorDonation); val color = CSSColor.parseColor(donation.colorDonation);
root.background.setTint(color); 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); _textAmount.setTextColor(Color.BLACK);
else else
_textAmount.setTextColor(Color.WHITE); _textAmount.setTextColor(Color.WHITE);
@@ -18,6 +18,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.overlays.LiveChatOverlay import com.futo.platformplayer.views.overlays.LiveChatOverlay
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import toAndroidColor
class LiveChatMessageListItem(viewGroup: ViewGroup) class LiveChatMessageListItem(viewGroup: ViewGroup)
: LiveChatListItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chat_message, viewGroup, false)) { : 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()) { if (!event.colorName.isNullOrEmpty()) {
try { try {
_authorName.setTextColor(Color.parseColor(event.colorName)); _authorName.setTextColor(CSSColor.parseColor(event.colorName).toAndroidColor());
} catch (ex: Throwable) { } catch (ex: Throwable) {
} }
} else } else
@@ -14,9 +14,6 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout 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.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -43,6 +40,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import toAndroidColor
class LiveChatOverlay : LinearLayout { class LiveChatOverlay : LinearLayout {
@@ -66,10 +64,11 @@ class LiveChatOverlay : LinearLayout {
private val _overlayRaid: ConstraintLayout; private val _overlayRaid: ConstraintLayout;
private val _overlayRaid_Name: TextView; private val _overlayRaid_Name: TextView;
private val _overlayRaid_Message: TextView;
private val _overlayRaid_Thumbnail: ImageView; private val _overlayRaid_Thumbnail: ImageView;
private val _overlayRaid_ButtonGo: Button; private val _overlayRaid_ButtonGo: Button;
private val _overlayRaid_ButtonPrevent: Button; private val _overlayRaid_ButtonDismiss: Button;
private val _textViewers: TextView; private val _textViewers: TextView;
@@ -148,9 +147,10 @@ class LiveChatOverlay : LinearLayout {
_overlayRaid = findViewById(R.id.overlay_raid); _overlayRaid = findViewById(R.id.overlay_raid);
_overlayRaid_Name = findViewById(R.id.raid_name); _overlayRaid_Name = findViewById(R.id.raid_name);
_overlayRaid_Message = findViewById(R.id.textRaidMessage);
_overlayRaid_Thumbnail = findViewById(R.id.raid_thumbnail); _overlayRaid_Thumbnail = findViewById(R.id.raid_thumbnail);
_overlayRaid_ButtonGo = findViewById(R.id.raid_button_go); _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; _overlayRaid.visibility = View.GONE;
@@ -159,7 +159,7 @@ class LiveChatOverlay : LinearLayout {
onRaidNow.emit(it); onRaidNow.emit(it);
} }
} }
_overlayRaid_ButtonPrevent.setOnClickListener { _overlayRaid_ButtonDismiss.setOnClickListener {
_currentRaid?.let { _currentRaid?.let {
_currentRaid = null; _currentRaid = null;
_overlayRaid.visibility = View.GONE; _overlayRaid.visibility = View.GONE;
@@ -291,10 +291,10 @@ class LiveChatOverlay : LinearLayout {
_overlayDonation_Amount.text = donation.amount.trim(); _overlayDonation_Amount.text = donation.amount.trim();
_overlayDonation.visibility = VISIBLE; _overlayDonation.visibility = VISIBLE;
if(donation.colorDonation != null && donation.colorDonation.isHexColor()) { if(donation.colorDonation != null && donation.colorDonation.isHexColor()) {
val color = Color.parseColor(donation.colorDonation); val color = CSSColor.parseColor(donation.colorDonation);
_overlayDonation_AmountContainer.background.setTint(color); _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) _overlayDonation_Amount.setTextColor(Color.BLACK)
else else
_overlayDonation_Amount.setTextColor(Color.WHITE); _overlayDonation_Amount.setTextColor(Color.WHITE);
@@ -372,6 +372,15 @@ class LiveChatOverlay : LinearLayout {
} }
else else
_overlayRaid.visibility = View.GONE; _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) { fun setViewCount(viewCount: Int) {
@@ -13,6 +13,7 @@ import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
@@ -42,10 +43,14 @@ class SlideUpMenuOverlay : RelativeLayout {
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){ constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){
init(animated, okText); init(animated, okText);
_container = parent; _container = parent;
if(!_container!!.children.contains(this)) { _container!!.removeAllViews();
_container!!.removeAllViews(); _container!!.addView(this);
_container!!.addView(this); if (_container!!.isVisible) {
isVisible = true
_viewBackground.alpha = 1.0f;
_viewOverlayContainer.translationY = 0.0f;
} }
_textTitle.text = titleText; _textTitle.text = titleText;
groupItems = items; groupItems = items;
@@ -56,6 +61,12 @@ class SlideUpMenuOverlay : RelativeLayout {
} }
setItems(items); setItems(items);
if (!isVisible) {
_viewOverlayContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
_viewOverlayContainer.translationY = _viewOverlayContainer.measuredHeight.toFloat()
_viewBackground.alpha = 0f;
}
} }
@@ -146,16 +157,9 @@ class SlideUpMenuOverlay : RelativeLayout {
} }
isVisible = true; isVisible = true;
_container?.post { _container?.visibility = View.VISIBLE;
_container?.visibility = View.VISIBLE;
_container?.bringToFront();
}
if (_animated) { if (_animated) {
_viewOverlayContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
_viewOverlayContainer.translationY = _viewOverlayContainer.measuredHeight.toFloat()
_viewBackground.alpha = 0f;
val animations = arrayListOf<Animator>(); val animations = arrayListOf<Animator>();
animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 0.0f, 1.0f).setDuration(ANIMATION_DURATION_MS)); animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 0.0f, 1.0f).setDuration(ANIMATION_DURATION_MS));
animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", _viewOverlayContainer.measuredHeight.toFloat(), 0.0f).setDuration(ANIMATION_DURATION_MS)); animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", _viewOverlayContainer.measuredHeight.toFloat(), 0.0f).setDuration(ANIMATION_DURATION_MS));
@@ -1,5 +1,6 @@
package com.futo.platformplayer.views.video package com.futo.platformplayer.views.video
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
@@ -43,9 +44,13 @@ import com.futo.platformplayer.formatDuration
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import com.futo.platformplayer.views.others.ProgressBar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -117,6 +122,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private var _isControlsLocked: Boolean = false; private var _isControlsLocked: Boolean = false;
private var _speedHoldPrevRate = 1f
private var _speedHoldWasPlaying = false
private val _time_bar_listener: TimeBar.OnScrubListener; private val _time_bar_listener: TimeBar.OnScrubListener;
var isFitMode : Boolean = false var isFitMode : Boolean = false
@@ -147,6 +155,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onChapterClicked = Event1<IChapter>(); val onChapterClicked = Event1<IChapter>();
private val _loaderGame: TargetTapLoaderView
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
LayoutInflater.from(context).inflate(R.layout.video_view, this, true); LayoutInflater.from(context).inflate(R.layout.video_view, this, true);
@@ -187,6 +197,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration); _control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration);
_control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause); _control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause);
_loaderGame = findViewById(R.id.loader_overlay)
_loaderGame.visibility = View.GONE
_control_chapter.setOnClickListener { _control_chapter.setOnClickListener {
_currentChapter?.let { _currentChapter?.let {
onChapterClicked.emit(it); onChapterClicked.emit(it);
@@ -254,6 +267,20 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
gestureControl = findViewById(R.id.gesture_control); gestureControl = findViewById(R.id.gesture_control);
gestureControl.setupTouchArea(_layoutControls, background); gestureControl.setupTouchArea(_layoutControls, background);
gestureControl.onSpeedHoldStart.subscribe {
exoPlayer?.player?.let { player ->
_speedHoldWasPlaying = player.isPlaying
_speedHoldPrevRate = getPlaybackRate()
setPlaybackRate(Settings.instance.playback.getHoldPlaybackSpeed().toFloat())
player.play()
}
}
gestureControl.onSpeedHoldEnd.subscribe {
exoPlayer?.player?.let { player ->
if (!_speedHoldWasPlaying) player.pause()
setPlaybackRate(_speedHoldPrevRate)
}
}
gestureControl.onSeek.subscribe { seekFromCurrent(it); }; gestureControl.onSeek.subscribe { seekFromCurrent(it); };
gestureControl.onSoundAdjusted.subscribe { gestureControl.onSoundAdjusted.subscribe {
if (Settings.instance.gestureControls.useSystemVolume) { if (Settings.instance.gestureControls.useSystemVolume) {
@@ -848,4 +875,19 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
override fun onSurfaceSizeChanged(width: Int, height: Int) { override fun onSurfaceSizeChanged(width: Int, height: Int) {
gestureControl.resetZoomPan() gestureControl.resetZoomPan()
} }
override fun setLoading(isLoading: Boolean) {
if (isLoading) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader()
} else {
_loaderGame.visibility = View.GONE
_loaderGame.stopAndResetLoader()
}
}
override fun setLoading(expectedDurationMs: Int) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader(expectedDurationMs.toLong())
}
} }
@@ -3,10 +3,12 @@ package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
@@ -29,6 +31,8 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.source.SingleSampleMediaSource import androidx.media3.exoplayer.source.SingleSampleMediaSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@@ -52,20 +56,31 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
import getHttpDataSourceFactory import getHttpDataSourceFactory
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.abs import kotlin.math.abs
abstract class FutoVideoPlayerBase : RelativeLayout { abstract class FutoVideoPlayerBase : RelativeLayout {
@@ -108,9 +123,12 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
val onPositionDiscontinuity = Event1<Long>(); val onPositionDiscontinuity = Event1<Long>();
val onDatasourceError = Event1<Throwable>(); val onDatasourceError = Event1<Throwable>();
val onReloadRequired = Event0();
private var _didCallSourceChange = false; private var _didCallSourceChange = false;
private var _lastState: Int = -1; private var _lastState: Int = -1;
private val _swapIdAudio = AtomicInteger(0)
private val _swapIdVideo = AtomicInteger(0)
var targetTrackVideoHeight = -1 var targetTrackVideoHeight = -1
private set private set
@@ -348,8 +366,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
var videoSourceUsed = videoSource; var videoSourceUsed = videoSource;
var audioSourceUsed = audioSource; var audioSourceUsed = audioSource;
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){ if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource); videoSource.getUnderlyingPlugin()?.busy {
audioSourceUsed = null; videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource);
audioSourceUsed = null;
}
} }
val didSetVideo = swapSourceInternal(videoSourceUsed, play, resume); val didSetVideo = swapSourceInternal(videoSourceUsed, play, resume);
@@ -427,13 +447,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean { private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean {
setLoading(false)
val swapId = _swapIdVideo.incrementAndGet()
_lastGeneratedDash = null; _lastGeneratedDash = null;
val didSet = when(videoSource) { val didSet = when(videoSource) {
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; } is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; } is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true } is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true }
is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;} is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;}
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume); is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume, swapId);
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; } is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; } is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; } is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
@@ -444,11 +466,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return didSet; return didSet;
} }
private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean { private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean {
setLoading(false)
val swapId = _swapIdAudio.incrementAndGet()
val didSet = when(audioSource) { val didSet = when(audioSource) {
is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; } is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; }
is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; } is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; }
is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; } is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; }
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume); is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume, swapId);
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; } is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; } is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
null -> { _lastAudioMediaSource = null; true; } null -> { _lastAudioMediaSource = null; true; }
@@ -555,22 +579,41 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}.createMediaSource(MediaItem.fromUri(videoSource.url)) }.createMediaSource(MediaItem.fromUri(videoSource.url))
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean { private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean, swapId: Int): Boolean {
Logger.i(TAG, "Loading VideoSource [Dash]"); Logger.i(TAG, "Loading VideoSource [Dash]");
if(videoSource.hasGenerate) { if(videoSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
val scope = this;
var startId = -1;
try { try {
val generated = videoSource.generate(); val plugin = videoSource.getUnderlyingPlugin() ?: return@launch;
startId = plugin.getUnderlyingPlugin()?.runtimeId ?: -1;
val generatedDef = plugin.busy { videoSource.generateAsync(scope); };
withContext(Dispatchers.Main) {
if (generatedDef.estDuration >= 0) {
setLoading(generatedDef.estDuration)
} else {
setLoading(true)
}
}
val generated = generatedDef.await();
if (_swapIdVideo.get() != swapId) {
return@launch
}
withContext(Dispatchers.Main) {
setLoading(false)
}
if (generated != null) { if (generated != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource)) val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
videoSource.getHttpDataSourceFactory() withContext(Dispatchers.IO) { videoSource.getHttpDataSourceFactory() }
else else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource) if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource)
dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor()); dataSource.setRequestExecutor2(withContext(Dispatchers.IO){videoSource.audio.getRequestExecutor()});
_lastVideoMediaSource = DashMediaSource.Factory(dataSource) _lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource( .createMediaSource(
DashManifestParser().parse( DashManifestParser().parse(
@@ -585,8 +628,23 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
} }
} }
catch(reloadRequired: ScriptReloadRequiredException) {
Logger.i(TAG, "Reload required detected");
val plugin = videoSource.getUnderlyingPlugin();
if(plugin == null)
return@launch;
if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId)
return@launch;
StatePlatform.instance.handleReloadRequired(reloadRequired, {
onReloadRequired.emit();
});
}
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e(TAG, "DashRaw generator failed", ex); Logger.e(TAG, "DashRaw generator failed", ex);
} finally {
withContext(Dispatchers.Main) {
setLoading(false)
}
} }
} }
return false; return false;
@@ -669,27 +727,69 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean { private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean, swapId: Int): Boolean {
Logger.i(TAG, "Loading AudioSource [DashRaw]"); Logger.i(TAG, "Loading AudioSource [DashRaw]");
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
audioSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
if(audioSource.hasGenerate) { if(audioSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
val generated = audioSource.generate(); val scope = this;
if(generated != null) { var startId = -1;
try {
val plugin = audioSource.getUnderlyingPlugin() ?: return@launch;
startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1;
val generatedDef = plugin.busy { audioSource.generateAsync(scope); }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_lastVideoMediaSource = DashMediaSource.Factory(dataSource) if (generatedDef.estDuration >= 0) {
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url), setLoading(generatedDef.estDuration)
ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0)))); } else {
loadSelectedSources(play, resume); setLoading(true)
}
}
val generated = generatedDef.await();
if (_swapIdAudio.get() != swapId) {
return@launch
}
withContext(Dispatchers.Main) {
setLoading(false)
}
if(generated != null) {
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
audioSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
withContext(Dispatchers.Main) {
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url),
ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0))));
loadSelectedSources(play, resume);
}
}
}
catch(reloadRequired: ScriptReloadRequiredException) {
Logger.i(TAG, "Reload required detected");
val plugin = audioSource.getUnderlyingPlugin();
if(plugin == null)
return@launch;
if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId)
return@launch;
StatePlatform.instance.reEnableClient(plugin.id, {
onReloadRequired.emit();
});
}
catch(ex: Throwable) {
} finally {
withContext(Dispatchers.Main) {
setLoading(false)
} }
} }
} }
return false; return false;
} }
else { else {
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
audioSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastVideoMediaSource = DashMediaSource.Factory(dataSource) _lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource( .createMediaSource(
DashManifestParser().parse( DashManifestParser().parse(
@@ -811,6 +911,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
fun clear() { fun clear() {
exoPlayer?.player?.stop(); exoPlayer?.player?.stop();
exoPlayer?.player?.clearMediaItems(); exoPlayer?.player?.clearMediaItems();
setLoading(false)
_swapIdVideo.incrementAndGet()
_swapIdAudio.incrementAndGet()
_lastVideoMediaSource = null; _lastVideoMediaSource = null;
_lastAudioMediaSource = null; _lastAudioMediaSource = null;
_lastSubtitleMediaSource = null; _lastSubtitleMediaSource = null;
@@ -890,6 +993,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
} }
protected open fun setLoading(isLoading: Boolean) { }
protected open fun setLoading(expectedDurationMs: Int) { }
companion object { companion object {
val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; 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" app:buttonIcon="@drawable/ic_copy"
android:layout_marginTop="8dp" /> android:layout_marginTop="8dp" />
</LinearLayout> </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> </androidx.constraintlayout.widget.ConstraintLayout>
+3 -4
View File
@@ -5,9 +5,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/black"> android:background="@color/black">
<com.futo.platformplayer.views.others.CircularProgressBar <com.futo.platformplayer.views.TargetTapLoaderView
android:id="@+id/test_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="240dp" />
app:progress="0%"
app:strokeWidth="20dp" />
</FrameLayout> </FrameLayout>

Some files were not shown because too many files have changed in this diff Show More