mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
174 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ec7e821b | |||
| ca3454afbe | |||
| 1edc8aabf8 | |||
| 91060faac9 | |||
| 17027ba364 | |||
| 8569eaa5db | |||
| d32d817e0a | |||
| a0f4cc760c | |||
| 5247997ea5 | |||
| 453030d561 | |||
| e080702a52 | |||
| 3909343adc | |||
| dc76934d0e | |||
| 6cf47d592a | |||
| 1507c70729 | |||
| d6a23ac0de | |||
| 17df396672 | |||
| 0c5ba0cd39 | |||
| 183aeb18a0 | |||
| 8d08e19cd2 | |||
| a882d04d26 | |||
| c4d06c1ba2 | |||
| 4dfcd47901 | |||
| 4c0c1abb4b | |||
| 6f44071186 | |||
| 29910a2698 | |||
| b5da0d4462 | |||
| 99fb9b3462 | |||
| 5f0a89d13b | |||
| f311561e6f | |||
| 2fc944ddd9 | |||
| a2970b86ee | |||
| ac9a51f105 | |||
| 90dca2537a | |||
| 4df227147c | |||
| 1fb55dca0a | |||
| 3d7b347e49 | |||
| 769ec9f59a | |||
| dee310de3d | |||
| 0af4bad906 | |||
| 4731673ba3 | |||
| 8745221cbd | |||
| 742d95440e | |||
| 180b320cd7 | |||
| cc8dffc485 | |||
| a64fd2cf35 | |||
| 4aceb364d9 | |||
| 76d9bac0ec | |||
| 2b8dc41d0d | |||
| 33430c538c | |||
| 03e9cb398b | |||
| 2aef2ebec1 | |||
| 5e5fffbf97 | |||
| 51ac604e31 | |||
| 4e49b5bc63 | |||
| 658cbc5e00 | |||
| 2ceb4c5644 | |||
| 2738954af7 | |||
| db5aaf0b84 | |||
| e1abb7f8ae | |||
| 3310ac6008 | |||
| 09879c83e9 | |||
| 7aa8b6bc14 | |||
| cac8a8fde4 | |||
| 01cb544dfd | |||
| b9239b6177 | |||
| 96ca3f62a2 | |||
| 73ad783881 | |||
| 3bfcf65535 | |||
| 8b3b27a2a8 | |||
| a4d4835a89 | |||
| 56c0f7bfaf | |||
| 736424ae35 | |||
| 37dc778009 | |||
| cd3cea58a4 | |||
| 8b53e9e5e3 | |||
| 08e98b089c | |||
| 5528d71da8 | |||
| 83f520ca44 | |||
| cc247ce634 | |||
| c6caa59a90 | |||
| 00e28b9ce0 | |||
| 334f58979a | |||
| 940bf163da | |||
| 2bbe0e6133 | |||
| 861f34a287 | |||
| 82f214f155 | |||
| 4ee127fe13 | |||
| 86a4cf8d84 | |||
| 2c463dd5a1 | |||
| ed3820bec0 | |||
| 542a7f212d | |||
| 8fb0826d69 | |||
| deeaa55f56 | |||
| 5b954727a1 | |||
| fae77c1a63 | |||
| b69402dfe9 | |||
| 1f3e306a59 | |||
| a9605118fb | |||
| d22e918273 | |||
| bdcb94055a | |||
| d0644d39da | |||
| 8f3f776e22 | |||
| 548752e240 | |||
| 7f20250951 | |||
| 4d720b1d81 | |||
| 1e4aefb7d5 | |||
| 2a825a9f83 | |||
| a8921a1aba | |||
| edb9eda0a9 | |||
| 3a81676447 | |||
| 6695774037 | |||
| 03132ff77b | |||
| 49ddecdea4 | |||
| a10bc8c7de | |||
| c1e6e401cc | |||
| 44ff951ec6 | |||
| 11319e0ec5 | |||
| 100e98a960 | |||
| c6100ede70 | |||
| a2986a72bd | |||
| e0e90c5f74 | |||
| 11992af81b | |||
| 15d771f7fc | |||
| 5ede474253 | |||
| 7922aa6f80 | |||
| 0c1333fa15 | |||
| 53b9ba0368 | |||
| c3a8877796 | |||
| a464ae9df5 | |||
| 98b6213886 | |||
| b6671c653c | |||
| 55d042bee3 | |||
| 0d16dd0006 | |||
| 48a96140a7 | |||
| 603ef8f295 | |||
| ab07288ba0 | |||
| c0bbe5d491 | |||
| b953ff21e7 | |||
| c14378b534 | |||
| 80034ad131 | |||
| 33d3d9a29c | |||
| 7e83793586 | |||
| 6ba9ec8bc2 | |||
| 0b02ab0e2d | |||
| ff531b5e77 | |||
| b3f9de3b83 | |||
| 86bd71b89c | |||
| 2fca7e9a01 | |||
| 58c9aeb1a2 | |||
| 4702787784 | |||
| 30c41044da | |||
| e369676808 | |||
| 2fa9e65bee | |||
| cf96bd1ec0 | |||
| 1f5a069877 | |||
| adc5013ea4 | |||
| 515c5e00e9 | |||
| ba9f843368 | |||
| 0653f88c49 | |||
| 4ce9f64808 | |||
| 4fa0229ccb | |||
| 42dd8d6152 | |||
| 0a839b4814 | |||
| 586db317dd | |||
| ae36a24ad1 | |||
| 9a435f8859 | |||
| 81162c5df2 | |||
| c7c3ddfc96 | |||
| 830d3a9022 | |||
| a1c2d19daf | |||
| bd87a47551 | |||
| 76103a2a8c | |||
| f63f9dd6db |
@@ -26,7 +26,7 @@ body:
|
|||||||
label: Reproduction steps
|
label: Reproduction steps
|
||||||
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
|
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
0. Play a Youtube video
|
0. Play a YouTube video
|
||||||
1. Press on Download button
|
1. Press on Download button
|
||||||
2. Select quality 1440p
|
2. Select quality 1440p
|
||||||
3. Grayjay crashes when attempting to download
|
3. Grayjay crashes when attempting to download
|
||||||
@@ -83,7 +83,7 @@ body:
|
|||||||
- "Spotify"
|
- "Spotify"
|
||||||
- "TedTalks"
|
- "TedTalks"
|
||||||
- "Twitch"
|
- "Twitch"
|
||||||
- "Youtube"
|
- "YouTube"
|
||||||
- "Other"
|
- "Other"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -106,3 +106,9 @@
|
|||||||
[submodule "app/src/stable/assets/sources/crunchyroll"]
|
[submodule "app/src/stable/assets/sources/crunchyroll"]
|
||||||
path = app/src/stable/assets/sources/crunchyroll
|
path = app/src/stable/assets/sources/crunchyroll
|
||||||
url = ../plugins/crunchyroll.git
|
url = ../plugins/crunchyroll.git
|
||||||
|
[submodule "app/src/stable/assets/sources/mixcloud"]
|
||||||
|
path = app/src/stable/assets/sources/mixcloud
|
||||||
|
url = ../plugins/mixcloud.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/mixcloud"]
|
||||||
|
path = app/src/unstable/assets/sources/mixcloud
|
||||||
|
url = ../plugins/mixcloud.git
|
||||||
|
|||||||
+4
-2
@@ -154,9 +154,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.dagger:dagger:2.48'
|
//implementation 'com.google.dagger:dagger:2.48'
|
||||||
implementation 'androidx.test:monitor:1.7.2'
|
implementation 'androidx.test:monitor:1.7.2'
|
||||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
|
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
@@ -180,6 +181,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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -245,6 +251,9 @@ class PlatformVideo extends PlatformContent {
|
|||||||
this.duration = obj.duration ?? -1; //Long
|
this.duration = obj.duration ?? -1; //Long
|
||||||
this.viewCount = obj.viewCount ?? -1; //Long
|
this.viewCount = obj.viewCount ?? -1; //Long
|
||||||
|
|
||||||
|
this.playbackTime = obj.playbackTime ?? -1;
|
||||||
|
this.playbackDate = obj.playbackDate ?? undefined;
|
||||||
|
|
||||||
this.isLive = obj.isLive ?? false; //Boolean
|
this.isLive = obj.isLive ?? false; //Boolean
|
||||||
this.isShort = !!obj.isShort ?? false;
|
this.isShort = !!obj.isShort ?? false;
|
||||||
}
|
}
|
||||||
@@ -458,14 +467,20 @@ class AudioUrlWidevineSource extends AudioUrlSource {
|
|||||||
this.getLicenseRequestExecutor = () => {
|
this.getLicenseRequestExecutor = () => {
|
||||||
return {
|
return {
|
||||||
executeRequest: (url, _headers, _method, license_request_data) => {
|
executeRequest: (url, _headers, _method, license_request_data) => {
|
||||||
return http.POST(
|
const response = http.POST(
|
||||||
url,
|
url,
|
||||||
license_request_data,
|
license_request_data,
|
||||||
{ Authorization: `Bearer ${obj.bearerToken}` },
|
{ Authorization: `Bearer ${obj.bearerToken}` },
|
||||||
false,
|
false,
|
||||||
true
|
true
|
||||||
).body
|
);
|
||||||
}
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new ScriptException("Unable to acquire license key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -701,11 +716,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,6 +794,7 @@ let plugin = {
|
|||||||
//To override by plugin
|
//To override by plugin
|
||||||
const source = {
|
const source = {
|
||||||
getHome() { return new ContentPager([], false, {}); },
|
getHome() { return new ContentPager([], false, {}); },
|
||||||
|
getShorts() { return new VideoPager([], false, {}); },
|
||||||
|
|
||||||
enable(config){ },
|
enable(config){ },
|
||||||
disable() {},
|
disable() {},
|
||||||
|
|||||||
@@ -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 0–1 --
|
||||||
|
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 0–255 --
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
@@ -587,21 +587,27 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
|
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
|
||||||
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
|
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
|
||||||
var holdPlaybackSpeed: Int = 3;
|
var holdPlaybackSpeed: Int = 4;
|
||||||
|
|
||||||
fun getHoldPlaybackSpeed(): Double {
|
fun getHoldPlaybackSpeed(): Double {
|
||||||
return when(holdPlaybackSpeed) {
|
return when(holdPlaybackSpeed) {
|
||||||
0 -> 1.25
|
0 -> 1.0
|
||||||
1 -> 1.5
|
1 -> 1.25
|
||||||
2 -> 1.75
|
2 -> 1.5
|
||||||
3 -> 2.0
|
3 -> 1.75
|
||||||
4 -> 2.25
|
4 -> 2.0
|
||||||
5 -> 2.5
|
5 -> 2.25
|
||||||
6 -> 2.75
|
6 -> 2.5
|
||||||
7 -> 3.0
|
7 -> 2.75
|
||||||
|
8 -> 3.0
|
||||||
else -> 2.0
|
else -> 2.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
|
||||||
|
var shortsPregenerate: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
@@ -1017,8 +1023,8 @@ 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.add_to_beginning_of_watch_later, FieldForm.TOGGLE, R.string.add_to_beginning_description, 4)
|
@FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
|
||||||
var addToBeginning: Boolean = true;
|
var watchLaterAddStart: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
|
@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;
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showCastingDialog(context: Context) {
|
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
val dialog = ConnectedCastingDialog(context);
|
val dialog = ConnectedCastingDialog(context);
|
||||||
@@ -432,6 +432,7 @@ class UIDialogs {
|
|||||||
dialog.setOwnerActivity(context)
|
dialog.setOwnerActivity(context)
|
||||||
}
|
}
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
} else {
|
} else {
|
||||||
@@ -444,21 +445,24 @@ class UIDialogs {
|
|||||||
if (c is Activity) {
|
if (c is Activity) {
|
||||||
dialog.setOwnerActivity(c);
|
dialog.setOwnerActivity(c);
|
||||||
}
|
}
|
||||||
|
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCastingTutorialDialog(context: Context) {
|
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
||||||
val dialog = CastingHelpDialog(context);
|
val dialog = CastingHelpDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCastingAddDialog(context: Context) {
|
fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) {
|
||||||
val dialog = CastingAddDialog(context);
|
val dialog = CastingAddDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,115 +129,163 @@ class UISlideOverlays {
|
|||||||
val originalVideo = subscription.doFetchVideos;
|
val originalVideo = subscription.doFetchVideos;
|
||||||
val originalPosts = subscription.doFetchPosts;
|
val originalPosts = subscription.doFetchPosts;
|
||||||
|
|
||||||
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
val menu = SlideUpMenuOverlay(
|
||||||
|
container.context,
|
||||||
|
container,
|
||||||
|
"Subscription Settings",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
listOf()
|
||||||
|
);
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
try {
|
||||||
val capabilities = plugin.getChannelCapabilities();
|
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||||
|
val capabilities = plugin.getChannelCapabilities();
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
items.addAll(listOf(
|
items.addAll(
|
||||||
SlideUpMenuItem(
|
listOf(
|
||||||
container.context,
|
SlideUpMenuItem(
|
||||||
R.drawable.ic_notifications,
|
container.context,
|
||||||
"Notifications",
|
R.drawable.ic_notifications,
|
||||||
"",
|
"Notifications",
|
||||||
tag = "notifications",
|
"",
|
||||||
call = {
|
tag = "notifications",
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
call = {
|
||||||
},
|
subscription.doNotifications =
|
||||||
invokeParent = false
|
menu?.selectOption(null, "notifications", true, true)
|
||||||
),
|
?: subscription.doNotifications;
|
||||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
},
|
||||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
invokeParent = false
|
||||||
"You can select which groups this subscription is part of.",
|
),
|
||||||
-1, listOf()) else null,
|
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
.isNotEmpty()
|
||||||
SlideUpMenuRecycler(container.context, "as") {
|
)
|
||||||
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
SlideUpMenuGroup(
|
||||||
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
container.context, "Subscription Groups",
|
||||||
.sortedBy { !it.selected });
|
"You can select which groups this subscription is part of.",
|
||||||
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
|
-1, listOf()
|
||||||
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
) else null,
|
||||||
it.onClick.subscribe {
|
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
if(it is SubscriptionGroup.Selectable) {
|
.isNotEmpty()
|
||||||
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
|
)
|
||||||
?: return@subscribe;
|
SlideUpMenuRecycler(container.context, "as") {
|
||||||
groups.clear();
|
val groups =
|
||||||
if(it.selected)
|
ArrayList<SubscriptionGroup>(
|
||||||
actualGroup.urls.remove(subscription.channel.url);
|
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
else
|
.map {
|
||||||
actualGroup.urls.add(subscription.channel.url);
|
SubscriptionGroup.Selectable(
|
||||||
|
it,
|
||||||
|
it.urls.contains(subscription.channel.url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sortedBy { !it.selected });
|
||||||
|
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? =
|
||||||
|
null;
|
||||||
|
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if (it is SubscriptionGroup.Selectable) {
|
||||||
|
val actualGroup =
|
||||||
|
StateSubscriptionGroups.instance.getSubscriptionGroup(
|
||||||
|
it.id
|
||||||
|
)
|
||||||
|
?: return@subscribe;
|
||||||
|
groups.clear();
|
||||||
|
if (it.selected)
|
||||||
|
actualGroup.urls.remove(subscription.channel.url);
|
||||||
|
else
|
||||||
|
actualGroup.urls.add(subscription.channel.url);
|
||||||
|
|
||||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
|
StateSubscriptionGroups.instance.updateSubscriptionGroup(
|
||||||
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
actualGroup
|
||||||
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
);
|
||||||
.sortedBy { !it.selected });
|
groups.addAll(
|
||||||
adapter?.notifyContentChanged();
|
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
}
|
.map {
|
||||||
}
|
SubscriptionGroup.Selectable(
|
||||||
};
|
it,
|
||||||
return@SlideUpMenuRecycler adapter;
|
it.urls.contains(subscription.channel.url)
|
||||||
} else null,
|
)
|
||||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
}
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
.sortedBy { !it.selected });
|
||||||
-1, listOf()),
|
adapter?.notifyContentChanged();
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
}
|
||||||
container.context,
|
}
|
||||||
R.drawable.ic_live_tv,
|
};
|
||||||
"Livestreams",
|
return@SlideUpMenuRecycler adapter;
|
||||||
"Check for livestreams",
|
} else null,
|
||||||
tag = "fetchLive",
|
SlideUpMenuGroup(
|
||||||
call = {
|
container.context, "Fetch Settings",
|
||||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
},
|
-1, listOf()
|
||||||
invokeParent = false
|
),
|
||||||
) else null,
|
if (capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
container.context,
|
||||||
container.context,
|
R.drawable.ic_live_tv,
|
||||||
R.drawable.ic_play,
|
"Livestreams",
|
||||||
"Streams",
|
"Check for livestreams",
|
||||||
"Check for streams",
|
tag = "fetchLive",
|
||||||
tag = "fetchStreams",
|
call = {
|
||||||
call = {
|
subscription.doFetchLive =
|
||||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
menu?.selectOption(null, "fetchLive", true, true)
|
||||||
},
|
?: subscription.doFetchLive;
|
||||||
invokeParent = false
|
},
|
||||||
) else null,
|
invokeParent = false
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
) else null,
|
||||||
SlideUpMenuItem(
|
if (capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
"Videos",
|
"Streams",
|
||||||
"Check for videos",
|
"Check for streams",
|
||||||
tag = "fetchVideos",
|
tag = "fetchStreams",
|
||||||
call = {
|
call = {
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
subscription.doFetchStreams =
|
||||||
},
|
menu?.selectOption(null, "fetchStreams", true, true)
|
||||||
invokeParent = false
|
?: subscription.doFetchStreams;
|
||||||
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
},
|
||||||
SlideUpMenuItem(
|
invokeParent = false
|
||||||
container.context,
|
) else null,
|
||||||
R.drawable.ic_play,
|
if (capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
"Content",
|
SlideUpMenuItem(
|
||||||
"Check for content",
|
container.context,
|
||||||
tag = "fetchVideos",
|
R.drawable.ic_play,
|
||||||
call = {
|
"Videos",
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
"Check for videos",
|
||||||
},
|
tag = "fetchVideos",
|
||||||
invokeParent = false
|
call = {
|
||||||
) else null,
|
subscription.doFetchVideos =
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
menu?.selectOption(null, "fetchVideos", true, true)
|
||||||
container.context,
|
?: subscription.doFetchVideos;
|
||||||
R.drawable.ic_chat,
|
},
|
||||||
"Posts",
|
invokeParent = false
|
||||||
"Check for posts",
|
) else if (capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||||
tag = "fetchPosts",
|
SlideUpMenuItem(
|
||||||
call = {
|
container.context,
|
||||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
R.drawable.ic_play,
|
||||||
},
|
"Content",
|
||||||
invokeParent = false
|
"Check for content",
|
||||||
) else null/*,,
|
tag = "fetchVideos",
|
||||||
|
call = {
|
||||||
|
subscription.doFetchVideos =
|
||||||
|
menu?.selectOption(null, "fetchVideos", true, true)
|
||||||
|
?: subscription.doFetchVideos;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else null,
|
||||||
|
if (capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_chat,
|
||||||
|
"Posts",
|
||||||
|
"Check for posts",
|
||||||
|
tag = "fetchPosts",
|
||||||
|
call = {
|
||||||
|
subscription.doFetchPosts =
|
||||||
|
menu?.selectOption(null, "fetchPosts", true, true)
|
||||||
|
?: subscription.doFetchPosts;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else null/*,,
|
||||||
|
|
||||||
SlideUpMenuGroup(container.context, "Actions",
|
SlideUpMenuGroup(container.context, "Actions",
|
||||||
"Various things you can do with this subscription",
|
"Various things you can do with this subscription",
|
||||||
@@ -245,61 +293,82 @@ class UISlideOverlays {
|
|||||||
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
||||||
showCreateSubscriptionGroup(container, subscription.channel);
|
showCreateSubscriptionGroup(container, subscription.channel);
|
||||||
}, false)*/
|
}, false)*/
|
||||||
).filterNotNull());
|
).filterNotNull()
|
||||||
|
);
|
||||||
|
|
||||||
menu.setItems(items);
|
menu.setItems(items);
|
||||||
|
|
||||||
if(subscription.doNotifications)
|
if (subscription.doNotifications)
|
||||||
menu.selectOption(null, "notifications", true, true);
|
menu.selectOption(null, "notifications", true, true);
|
||||||
if(subscription.doFetchLive)
|
if (subscription.doFetchLive)
|
||||||
menu.selectOption(null, "fetchLive", true, true);
|
menu.selectOption(null, "fetchLive", true, true);
|
||||||
if(subscription.doFetchStreams)
|
if (subscription.doFetchStreams)
|
||||||
menu.selectOption(null, "fetchStreams", true, true);
|
menu.selectOption(null, "fetchStreams", true, true);
|
||||||
if(subscription.doFetchVideos)
|
if (subscription.doFetchVideos)
|
||||||
menu.selectOption(null, "fetchVideos", true, true);
|
menu.selectOption(null, "fetchVideos", true, true);
|
||||||
if(subscription.doFetchPosts)
|
if (subscription.doFetchPosts)
|
||||||
menu.selectOption(null, "fetchPosts", true, true);
|
menu.selectOption(null, "fetchPosts", true, true);
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
subscription.save();
|
subscription.save();
|
||||||
menu.hide(true);
|
menu.hide(true);
|
||||||
|
|
||||||
if(subscription.doNotifications && !originalNotif) {
|
if (subscription.doNotifications && !originalNotif) {
|
||||||
val mainContext = StateApp.instance.contextOrNull;
|
val mainContext = StateApp.instance.contextOrNull;
|
||||||
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
if (Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
||||||
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
|
UIDialogs.toast(
|
||||||
|
container.context,
|
||||||
|
"Enable 'Background Update' in settings for notifications to work"
|
||||||
|
);
|
||||||
|
|
||||||
if(mainContext is MainActivity) {
|
if (mainContext is MainActivity) {
|
||||||
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
|
UIDialogs.showDialog(
|
||||||
"You need to set a Background Updating interval for notifications", null, 0,
|
mainContext,
|
||||||
UIDialogs.Action("Cancel", {}),
|
R.drawable.ic_settings,
|
||||||
UIDialogs.Action("Configure", {
|
"Background Updating Required",
|
||||||
val intent = Intent(mainContext, SettingsActivity::class.java);
|
"You need to set a Background Updating interval for notifications",
|
||||||
intent.putExtra("query", mainContext.getString(R.string.background_update));
|
null,
|
||||||
mainContext.startActivity(intent);
|
0,
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
UIDialogs.Action("Cancel", {}),
|
||||||
}
|
UIDialogs.Action("Configure", {
|
||||||
return@subscribe;
|
val intent = Intent(
|
||||||
}
|
mainContext,
|
||||||
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
SettingsActivity::class.java
|
||||||
UIDialogs.toast(container.context, "Android notifications are disabled");
|
);
|
||||||
if(mainContext is MainActivity) {
|
intent.putExtra(
|
||||||
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
"query",
|
||||||
|
mainContext.getString(R.string.background_update)
|
||||||
|
);
|
||||||
|
mainContext.startActivity(intent);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return@subscribe;
|
||||||
|
} else if (!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
||||||
|
UIDialogs.toast(
|
||||||
|
container.context,
|
||||||
|
"Android notifications are disabled"
|
||||||
|
);
|
||||||
|
if (mainContext is MainActivity) {
|
||||||
|
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
menu.onCancel.subscribe {
|
||||||
menu.onCancel.subscribe {
|
subscription.doNotifications = originalNotif;
|
||||||
subscription.doNotifications = originalNotif;
|
subscription.doFetchLive = originalLive;
|
||||||
subscription.doFetchLive = originalLive;
|
subscription.doFetchStreams = originalStream;
|
||||||
subscription.doFetchStreams = originalStream;
|
subscription.doFetchVideos = originalVideo;
|
||||||
subscription.doFetchVideos = originalVideo;
|
subscription.doFetchPosts = originalPosts;
|
||||||
subscription.doFetchPosts = originalPosts;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
menu.setOk("Save");
|
menu.setOk("Save");
|
||||||
|
|
||||||
menu.show();
|
menu.show();
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to show subscription overlay.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.matchesDomain
|
||||||
import com.futo.platformplayer.others.LoginWebViewClient
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -74,6 +75,11 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
var isFirstLoad = true;
|
var isFirstLoad = true;
|
||||||
|
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
|
||||||
|
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
|
||||||
|
var currentScale = 100;
|
||||||
|
var currentDesktop = false;
|
||||||
|
_webView.setInitialScale(50);
|
||||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||||
_textUrl.setText(url ?: "");
|
_textUrl.setText(url ?: "");
|
||||||
|
|
||||||
@@ -86,6 +92,48 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
//TODO: Find most reliable way to wait for page js to finish
|
//TODO: Find most reliable way to wait for page js to finish
|
||||||
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(loginWarnings.size > 0 && url != null) {
|
||||||
|
synchronized(loginWarnings) {
|
||||||
|
val warning = loginWarnings.find { url.matches(it.getRegex()) };
|
||||||
|
if(warning != null) {
|
||||||
|
if(warning.once == true)
|
||||||
|
loginWarnings.remove(warning);
|
||||||
|
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
|
||||||
|
UIDialogs.Action("Understood", {
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
var specifiedScale = false;
|
||||||
|
var specifiedDesktop = false;
|
||||||
|
if(uiMods.size > 0 && url != null) {
|
||||||
|
synchronized(uiMods) {
|
||||||
|
val uimod = uiMods.find { url.matches(it.getRegex()) };
|
||||||
|
if(uimod != null) {
|
||||||
|
if(uimod.scale != null) {
|
||||||
|
currentScale =(uimod.scale * 100).toInt();
|
||||||
|
_webView.setInitialScale(currentScale);
|
||||||
|
specifiedScale = true;
|
||||||
|
}
|
||||||
|
if(uimod.desktop != null && uimod.desktop) {
|
||||||
|
_webView.settings.useWideViewPort = true;
|
||||||
|
specifiedDesktop = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!specifiedScale && currentScale != 100) {
|
||||||
|
currentScale = (100).toInt();
|
||||||
|
_webView.setInitialScale(currentScale);
|
||||||
|
}
|
||||||
|
if(!specifiedDesktop && currentDesktop) {
|
||||||
|
_webView.settings.useWideViewPort = false;
|
||||||
|
currentDesktop = false;
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
_webView.settings.domStorageEnabled = true;
|
_webView.settings.domStorageEnabled = true;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -63,6 +62,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsF
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||||
@@ -114,7 +114,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
|
||||||
|
|
||||||
@@ -171,6 +170,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
|
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
|
||||||
lateinit var _fragWatchlist: WatchLaterFragment;
|
lateinit var _fragWatchlist: WatchLaterFragment;
|
||||||
lateinit var _fragHistory: HistoryFragment;
|
lateinit var _fragHistory: HistoryFragment;
|
||||||
|
lateinit var _fragShorts: ShortsFragment;
|
||||||
lateinit var _fragSourceDetail: SourceDetailFragment;
|
lateinit var _fragSourceDetail: SourceDetailFragment;
|
||||||
lateinit var _fragDownloads: DownloadsFragment;
|
lateinit var _fragDownloads: DownloadsFragment;
|
||||||
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
||||||
@@ -340,6 +340,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragWebDetail = WebDetailFragment.newInstance();
|
_fragWebDetail = WebDetailFragment.newInstance();
|
||||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||||
_fragHistory = HistoryFragment.newInstance();
|
_fragHistory = HistoryFragment.newInstance();
|
||||||
|
_fragShorts = ShortsFragment.newInstance();
|
||||||
_fragSourceDetail = SourceDetailFragment.newInstance();
|
_fragSourceDetail = SourceDetailFragment.newInstance();
|
||||||
_fragDownloads = DownloadsFragment();
|
_fragDownloads = DownloadsFragment();
|
||||||
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
||||||
@@ -610,6 +611,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//startActivity(Intent(this, TestActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1253,6 +1256,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
WebDetailFragment::class -> _fragWebDetail as T;
|
WebDetailFragment::class -> _fragWebDetail as T;
|
||||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||||
HistoryFragment::class -> _fragHistory as T;
|
HistoryFragment::class -> _fragHistory as T;
|
||||||
|
ShortsFragment::class -> _fragShorts as T;
|
||||||
SourceDetailFragment::class -> _fragSourceDetail as T;
|
SourceDetailFragment::class -> _fragSourceDetail as T;
|
||||||
DownloadsFragment::class -> _fragDownloads as T;
|
DownloadsFragment::class -> _fragDownloads as T;
|
||||||
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;
|
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
|||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
|
|
||||||
@@ -36,6 +37,11 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getHome(): IPager<IPlatformContent>
|
fun getHome(): IPager<IPlatformContent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the shorts feed
|
||||||
|
*/
|
||||||
|
fun getShorts(): IPager<IPlatformVideo>
|
||||||
|
|
||||||
//Search
|
//Search
|
||||||
/**
|
/**
|
||||||
* Gets search suggestion for the provided query string
|
* Gets search suggestion for the provided query string
|
||||||
@@ -176,6 +182,10 @@ interface IPlatformClient {
|
|||||||
* Retrieves the subscriptions of the currently logged in user
|
* Retrieves the subscriptions of the currently logged in user
|
||||||
*/
|
*/
|
||||||
fun getUserSubscriptions(): Array<String>;
|
fun getUserSubscriptions(): Array<String>;
|
||||||
|
/**
|
||||||
|
* Retrieves the history of the currently logged in user
|
||||||
|
*/
|
||||||
|
fun getUserHistory(): IPager<IPlatformContent>;
|
||||||
|
|
||||||
|
|
||||||
fun isClaimTypeSupported(claimType: Int): Boolean;
|
fun isClaimTypeSupported(claimType: Int): Boolean;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
|||||||
import com.futo.platformplayer.api.media.models.live.LiveEventComment
|
import com.futo.platformplayer.api.media.models.live.LiveEventComment
|
||||||
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
|
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSVODEventPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -26,12 +27,17 @@ class LiveChatManager {
|
|||||||
private val _emojiCache: EmojiCache = EmojiCache();
|
private val _emojiCache: EmojiCache = EmojiCache();
|
||||||
private val _pager: IPager<IPlatformLiveEvent>?;
|
private val _pager: IPager<IPlatformLiveEvent>?;
|
||||||
|
|
||||||
|
private var _position: Long = 0;
|
||||||
|
private var _eventsPosition: Long = 0;
|
||||||
|
|
||||||
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
|
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
|
||||||
|
|
||||||
private var _startCounter = 0;
|
private var _startCounter = 0;
|
||||||
|
|
||||||
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
|
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
|
||||||
|
|
||||||
|
val isVOD get() = _pager is JSVODEventPager;
|
||||||
|
|
||||||
var viewCount: Long = 0
|
var viewCount: Long = 0
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
@@ -39,8 +45,24 @@ class LiveChatManager {
|
|||||||
_scope = scope;
|
_scope = scope;
|
||||||
_pager = pager;
|
_pager = pager;
|
||||||
viewCount = initialViewCount;
|
viewCount = initialViewCount;
|
||||||
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
if(pager is JSVODEventPager)
|
||||||
handleEvents(pager.getResults());
|
handleEvents(listOf(LiveEventComment("SYSTEM", null, "VOD chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
||||||
|
else
|
||||||
|
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
||||||
|
|
||||||
|
if(pager is JSVODEventPager) {
|
||||||
|
var replayResults = pager.getResults().filter { it.time > _eventsPosition || it is LiveEventEmojis };
|
||||||
|
//TODO: Remove this once dripfeed is done properly
|
||||||
|
replayResults = replayResults.filter{ it.time < _eventsPosition + 1500 || it is LiveEventEmojis };
|
||||||
|
if(replayResults.size > 0) {
|
||||||
|
_eventsPosition = replayResults.maxOf { it.time };
|
||||||
|
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_eventsPosition = _eventsPosition + 1500;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
handleEvents(pager.getResults());
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
@@ -52,6 +74,10 @@ class LiveChatManager {
|
|||||||
_startCounter++;
|
_startCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setVideoPosition(ms: Long) {
|
||||||
|
_position = ms;
|
||||||
|
}
|
||||||
|
|
||||||
fun getHistory(): List<IPlatformLiveEvent> {
|
fun getHistory(): List<IPlatformLiveEvent> {
|
||||||
synchronized(_history) {
|
synchronized(_history) {
|
||||||
return _history.toList();
|
return _history.toList();
|
||||||
@@ -85,13 +111,34 @@ class LiveChatManager {
|
|||||||
try {
|
try {
|
||||||
while(_startCounter == counter) {
|
while(_startCounter == counter) {
|
||||||
var nextInterval = 1000L;
|
var nextInterval = 1000L;
|
||||||
|
if(_pager is JSVODEventPager && _eventsPosition > _position) {
|
||||||
|
delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(_pager == null || !_pager.hasMorePages())
|
if(_pager == null || !_pager.hasMorePages())
|
||||||
return@launch;
|
return@launch;
|
||||||
_pager.nextPage();
|
val newEvents = if(_pager is JSVODEventPager) {
|
||||||
val newEvents = _pager.getResults();
|
val requestPosition = _position;
|
||||||
|
_pager.nextPage(requestPosition.toInt());
|
||||||
|
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
|
||||||
|
if(replayResults.size > 0) {
|
||||||
|
_eventsPosition = replayResults.maxOf { it.time };
|
||||||
|
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_eventsPosition = requestPosition + _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||||
|
replayResults;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_pager.nextPage();
|
||||||
|
_pager.getResults();
|
||||||
|
}
|
||||||
if(_pager is JSLiveEventPager)
|
if(_pager is JSLiveEventPager)
|
||||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||||
|
else if(_pager is JSVODEventPager)
|
||||||
|
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||||
|
|
||||||
if(newEvents.size > 0)
|
if(newEvents.size > 0)
|
||||||
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ data class PlatformClientCapabilities(
|
|||||||
val hasGetContentChapters: Boolean = false,
|
val hasGetContentChapters: Boolean = false,
|
||||||
val hasPeekChannelContents: Boolean = false,
|
val hasPeekChannelContents: Boolean = false,
|
||||||
val hasGetChannelPlaylists: Boolean = false,
|
val hasGetChannelPlaylists: Boolean = false,
|
||||||
val hasGetContentRecommendations: Boolean = false
|
val hasGetContentRecommendations: Boolean = false,
|
||||||
|
val hasGetUserHistory: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
+2
@@ -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,14 +2,17 @@ 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 {
|
||||||
val type : LiveEventType;
|
val type : LiveEventType;
|
||||||
|
var time: Long;
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -17,16 +18,21 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
|||||||
val colorName: String?;
|
val colorName: String?;
|
||||||
val badges: List<String>;
|
val badges: List<String>;
|
||||||
|
|
||||||
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null) {
|
override var time: Long = -1;
|
||||||
|
|
||||||
|
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null, time: Long = -1) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.thumbnail = thumbnail;
|
this.thumbnail = thumbnail;
|
||||||
this.colorName = colorName;
|
this.colorName = colorName;
|
||||||
this.badges = badges ?: listOf();
|
this.badges = badges ?: listOf();
|
||||||
|
this.time = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -36,7 +42,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
|||||||
obj.getOrThrow(config, "name", contextName),
|
obj.getOrThrow(config, "name", contextName),
|
||||||
obj.getOrThrow(config, "thumbnail", contextName, true),
|
obj.getOrThrow(config, "thumbnail", contextName, true),
|
||||||
obj.getOrThrow(config, "message", contextName),
|
obj.getOrThrow(config, "message", contextName),
|
||||||
colorName, badges);
|
colorName, badges,
|
||||||
|
obj.getOrDefault(config, "time", contextName, -1) ?: -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
|
|||||||
|
|
||||||
var expire: Int = 6000;
|
var expire: Int = 6000;
|
||||||
|
|
||||||
|
override var time: Long = -1;
|
||||||
|
|
||||||
|
|
||||||
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
|
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -37,6 +40,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 {
|
||||||
@@ -9,15 +10,17 @@ class LiveEventEmojis: IPlatformLiveEvent {
|
|||||||
|
|
||||||
val emojis: HashMap<String, String>;
|
val emojis: HashMap<String, String>;
|
||||||
|
|
||||||
|
override var time: Long = -1;
|
||||||
|
|
||||||
constructor(emojis: HashMap<String, String>) {
|
constructor(emojis: HashMap<String, String>) {
|
||||||
this.emojis = emojis;
|
this.emojis = emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,26 @@ 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) {
|
override var time: Long = -1;
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -9,12 +10,15 @@ class LiveEventViewCount: IPlatformLiveEvent {
|
|||||||
|
|
||||||
val viewCount: Int;
|
val viewCount: Int;
|
||||||
|
|
||||||
|
override var time: Long = -1;
|
||||||
|
|
||||||
constructor(viewCount: Int) {
|
constructor(viewCount: Int) {
|
||||||
this.viewCount = viewCount;
|
this.viewCount = viewCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
@@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.video
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A search result representing a video (overview data)
|
* A search result representing a video (overview data)
|
||||||
@@ -12,6 +13,9 @@ interface IPlatformVideo : IPlatformContent {
|
|||||||
val duration: Long;
|
val duration: Long;
|
||||||
val viewCount: Long;
|
val viewCount: Long;
|
||||||
|
|
||||||
|
val playbackTime: Long;
|
||||||
|
val playbackDate: OffsetDateTime?;
|
||||||
|
|
||||||
val isLive : Boolean;
|
val isLive : Boolean;
|
||||||
|
|
||||||
val isShort: Boolean;
|
val isShort: Boolean;
|
||||||
|
|||||||
+6
-3
@@ -3,11 +3,10 @@ package com.futo.platformplayer.api.media.models.video
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnail
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.polycentric.core.combineHashCodes
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonNames
|
import kotlinx.serialization.json.JsonNames
|
||||||
@@ -18,7 +17,7 @@ open class SerializedPlatformVideo(
|
|||||||
override val contentType: ContentType = ContentType.MEDIA,
|
override val contentType: ContentType = ContentType.MEDIA,
|
||||||
override val id: PlatformID,
|
override val id: PlatformID,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val thumbnails: Thumbnails,
|
override val thumbnails: Thumbnails = Thumbnails(),
|
||||||
override val author: PlatformAuthorLink,
|
override val author: PlatformAuthorLink,
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
@JsonNames("datetime", "dateTime")
|
@JsonNames("datetime", "dateTime")
|
||||||
@@ -33,6 +32,10 @@ open class SerializedPlatformVideo(
|
|||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
|
override var playbackTime: Long = -1;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override var playbackDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
override fun toJson() : String {
|
override fun toJson() : String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -13,7 +13,6 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
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
|
||||||
@@ -43,6 +42,10 @@ open class SerializedPlatformVideoDetails(
|
|||||||
) : IPlatformVideo, IPlatformVideoDetails {
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
|
|
||||||
|
override var playbackTime: Long = -1;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override var playbackDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
override val isLive: Boolean get() = false;
|
override val isLive: Boolean get() = false;
|
||||||
|
|
||||||
override val dash: IDashManifestSource? get() = null;
|
override val dash: IDashManifestSource? get() = null;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
|||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
||||||
@@ -43,6 +44,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoPager
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
@@ -59,9 +61,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 +89,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 +106,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>;
|
||||||
@@ -118,6 +126,7 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
|
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
|
||||||
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
||||||
|
val enableInShorts get() = descriptor.appSettings.tabEnabled.enableShorts ?: true
|
||||||
|
|
||||||
fun getSubscriptionRateLimit(): Int? {
|
fun getSubscriptionRateLimit(): Int? {
|
||||||
val pluginRateLimit = config.subscriptionRateLimit;
|
val pluginRateLimit = config.subscriptionRateLimit;
|
||||||
@@ -197,6 +206,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 +223,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;
|
||||||
@@ -245,7 +272,8 @@ open class JSClient : IPlatformClient {
|
|||||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
||||||
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
||||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
|
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false,
|
||||||
|
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -260,19 +288,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")
|
||||||
@@ -295,6 +332,13 @@ open class JSClient : IPlatformClient {
|
|||||||
plugin.executeTyped("source.getHome()"));
|
plugin.executeTyped("source.getHome()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JSDocs(2, "source.getShorts()", "Gets the Shorts feed of the platform")
|
||||||
|
override fun getShorts(): IPager<IPlatformVideo> = isBusyWith("getShorts") {
|
||||||
|
ensureEnabled()
|
||||||
|
return@isBusyWith JSVideoPager(config, this,
|
||||||
|
plugin.executeTyped("source.getShorts()"))
|
||||||
|
}
|
||||||
|
|
||||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||||
@@ -313,8 +357,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 +388,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 +423,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 +448,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 +562,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 +601,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 +643,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 +670,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
|
||||||
@@ -663,6 +713,13 @@ open class JSClient : IPlatformClient {
|
|||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JSOptional
|
||||||
|
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
|
||||||
|
override fun getUserHistory(): IPager<IPlatformContent> {
|
||||||
|
ensureEnabled();
|
||||||
|
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
|
||||||
|
}
|
||||||
|
|
||||||
fun validate() {
|
fun validate() {
|
||||||
try {
|
try {
|
||||||
plugin.start();
|
plugin.start();
|
||||||
@@ -734,19 +791,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 = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-3
@@ -1,6 +1,10 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.Dictionary
|
||||||
|
|
||||||
|
@Serializable
|
||||||
class SourcePluginAuthConfig(
|
class SourcePluginAuthConfig(
|
||||||
val loginUrl: String,
|
val loginUrl: String,
|
||||||
val completionUrl: String? = null,
|
val completionUrl: String? = null,
|
||||||
@@ -11,5 +15,44 @@ class SourcePluginAuthConfig(
|
|||||||
val userAgent: String? = null,
|
val userAgent: String? = null,
|
||||||
val loginButton: String? = null,
|
val loginButton: String? = null,
|
||||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||||
val loginWarning: String? = null
|
val loginWarning: String? = null,
|
||||||
) { }
|
val loginWarnings: List<Warning>? = null,
|
||||||
|
val uiMods: List<UIMod>? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Warning(
|
||||||
|
val url: String,
|
||||||
|
val text: String?,
|
||||||
|
val details: String? = null,
|
||||||
|
val once: Boolean? = true
|
||||||
|
) {
|
||||||
|
@Contextual
|
||||||
|
private var _regex: Regex? = null;
|
||||||
|
|
||||||
|
fun getRegex(): Regex {
|
||||||
|
return _regex ?: url.let {
|
||||||
|
val reg = Regex(it);
|
||||||
|
_regex = reg;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Serializable
|
||||||
|
class UIMod(
|
||||||
|
val url: String,
|
||||||
|
val scale: Float?,
|
||||||
|
val desktop: Boolean?
|
||||||
|
) {
|
||||||
|
@Contextual
|
||||||
|
private var _regex: Regex? = null;
|
||||||
|
|
||||||
|
fun getRegex(): Regex {
|
||||||
|
return _regex ?: url.let {
|
||||||
|
val reg = Regex(it);
|
||||||
|
_regex = reg;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ class SourcePluginConfig(
|
|||||||
var subscriptionRateLimit: Int? = null,
|
var subscriptionRateLimit: Int? = null,
|
||||||
var enableInSearch: Boolean = true,
|
var enableInSearch: Boolean = true,
|
||||||
var enableInHome: Boolean = true,
|
var enableInHome: Boolean = true,
|
||||||
|
var enableInShorts: Boolean = true,
|
||||||
var supportedClaimTypes: List<Int> = listOf(),
|
var supportedClaimTypes: List<Int> = listOf(),
|
||||||
var primaryClaimFieldType: Int? = null,
|
var primaryClaimFieldType: Int? = null,
|
||||||
var developerSubmitUrl: String? = null,
|
var developerSubmitUrl: String? = null,
|
||||||
|
|||||||
+20
-2
@@ -5,10 +5,16 @@ import com.futo.platformplayer.constructs.Event0
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -103,12 +109,22 @@ class SourcePluginDescriptor {
|
|||||||
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
|
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
|
||||||
var enableHome: Boolean? = null;
|
var enableHome: Boolean? = null;
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
|
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
|
||||||
var enableSearch: Boolean? = null;
|
var enableSearch: Boolean? = null;
|
||||||
|
|
||||||
|
@FormField(R.string.shorts, FieldForm.TOGGLE, R.string.show_content_in_shorts_tab, 3)
|
||||||
|
var enableShorts: Boolean? = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
|
@FormField(R.string.sync, "group", R.string.sync_desc, 3,"sync")
|
||||||
|
var sync = Sync();
|
||||||
|
@Serializable
|
||||||
|
class Sync {
|
||||||
|
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1,"syncHistory")
|
||||||
|
var enableHistorySync: Boolean? = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
|
||||||
var rateLimit = RateLimit();
|
var rateLimit = RateLimit();
|
||||||
@Serializable
|
@Serializable
|
||||||
class RateLimit {
|
class RateLimit {
|
||||||
@@ -143,6 +159,8 @@ class SourcePluginDescriptor {
|
|||||||
tabEnabled.enableHome = config.enableInHome
|
tabEnabled.enableHome = config.enableInHome
|
||||||
if(tabEnabled.enableSearch == null)
|
if(tabEnabled.enableSearch == null)
|
||||||
tabEnabled.enableSearch = config.enableInSearch
|
tabEnabled.enableSearch = config.enableInSearch
|
||||||
|
if(tabEnabled.enableShorts == null)
|
||||||
|
tabEnabled.enableShorts = config.enableInShorts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
@@ -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);
|
||||||
|
|||||||
+2
@@ -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);
|
||||||
|
|||||||
+3
-2
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-18
@@ -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> {
|
||||||
@@ -18,8 +19,8 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
protected var pager: V8ValueObject;
|
protected var pager: V8ValueObject;
|
||||||
|
|
||||||
private var _lastResults: List<T>? = null;
|
private var _lastResults: List<T>? = null;
|
||||||
private var _resultChanged: Boolean = true;
|
protected var _resultChanged: Boolean = true;
|
||||||
private var _hasMorePages: Boolean = false;
|
protected var _hasMorePages: Boolean = false;
|
||||||
//private var _morePagesWasFalse: Boolean = false;
|
//private var _morePagesWasFalse: Boolean = false;
|
||||||
|
|
||||||
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
|
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
|
||||||
@@ -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;
|
||||||
|
|||||||
+44
-23
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+60
-54
@@ -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() {
|
||||||
|
|||||||
+17
-10
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -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? {
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPlatformLiveEventPager
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.invokeV8
|
||||||
|
import com.futo.platformplayer.warnIfMainThread
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class JSVODEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
|
||||||
|
override var nextRequest: Int;
|
||||||
|
|
||||||
|
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {
|
||||||
|
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nextPage(ms: Int) = plugin.isBusyWith("JSLiveEventPager.nextPage") {
|
||||||
|
warnIfMainThread("VODEventPager.nextPage");
|
||||||
|
|
||||||
|
val pluginV8 = plugin.getUnderlyingPlugin();
|
||||||
|
pluginV8.busy {
|
||||||
|
val newPager: V8Value = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage(...)") {
|
||||||
|
pager.invokeV8<V8Value>("nextPage", ms);
|
||||||
|
};
|
||||||
|
if(newPager is V8ValueObject)
|
||||||
|
pager = newPager;
|
||||||
|
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||||
|
_resultChanged = true;
|
||||||
|
}
|
||||||
|
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun nextPage() = nextPage(0);
|
||||||
|
|
||||||
|
override fun convertResult(obj: V8ValueObject): IPlatformLiveEvent {
|
||||||
|
return IPlatformLiveEvent.fromV8(config, obj, "LiveEventPager");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
@@ -17,6 +21,10 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||||||
final override val duration: Long;
|
final override val duration: Long;
|
||||||
final override val viewCount: Long;
|
final override val viewCount: Long;
|
||||||
|
|
||||||
|
override var playbackTime: Long = -1;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override var playbackDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
final override val isLive: Boolean;
|
final override val isLive: Boolean;
|
||||||
final override val isShort: Boolean;
|
final override val isShort: Boolean;
|
||||||
|
|
||||||
@@ -29,5 +37,11 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||||||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||||
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||||
|
playbackTime = _content.getOrDefault<Long>(config, "playbackTime", contextName, -1)?.toLong() ?: -1;
|
||||||
|
val playbackDateInt = _content.getOrDefault<Int>(config, "playbackDate", contextName, null)?.toLong();
|
||||||
|
if(playbackDateInt == null || playbackDateInt == 0.toLong())
|
||||||
|
playbackDate = null;
|
||||||
|
else
|
||||||
|
playbackDate = OffsetDateTime.of(LocalDateTime.ofEpochSecond(playbackDateInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+39
-15
@@ -7,6 +7,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
@@ -24,12 +25,17 @@ 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
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
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;
|
||||||
|
private val _hasGetVODEvents: Boolean;
|
||||||
|
|
||||||
//Details
|
//Details
|
||||||
override val description : String;
|
override val description : String;
|
||||||
@@ -45,9 +51,9 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||||||
|
|
||||||
override val subtitles: List<ISubtitleSource>;
|
override val subtitles: List<ISubtitleSource>;
|
||||||
|
|
||||||
|
|
||||||
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));
|
||||||
@@ -69,6 +75,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||||||
_hasGetComments = _content.has("getComments");
|
_hasGetComments = _content.has("getComments");
|
||||||
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
||||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||||
|
_hasGetVODEvents = _content.has("getVODEvents");
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? {
|
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||||
@@ -82,14 +89,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 +115,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 +134,23 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasVODEvents(): Boolean{
|
||||||
|
return _hasGetVODEvents;
|
||||||
|
}
|
||||||
|
fun getVODEvents(url: String): IPager<IPlatformLiveEvent>? = _plugin.busy {
|
||||||
|
if(!_hasGetVODEvents)
|
||||||
|
return@busy null;
|
||||||
|
|
||||||
|
return@busy JSVODEventPager(_plugin.config, _plugin,
|
||||||
|
_content.invokeV8<V8ValueObject>("getVODEvents", arrayOf<Any>()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-1
@@ -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)
|
||||||
|
|||||||
+72
-8
@@ -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,14 @@ 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.logging.Logger
|
||||||
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 +58,56 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||||||
hasGenerate = _obj.has("generate");
|
hasGenerate = _obj.has("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var _pregenerate: V8Deferred<String?>? = null;
|
||||||
|
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||||
|
_pregenerate = generateAsync(scope);
|
||||||
|
return _pregenerate;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||||
|
if(!hasGenerate)
|
||||||
|
return V8Deferred(CompletableDeferred(manifest));
|
||||||
|
if(_obj.isClosed)
|
||||||
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
|
val pregenerated = _pregenerate;
|
||||||
|
if(pregenerated != null) {
|
||||||
|
Logger.w("JSDashManifestRawAudioSource", "Returning pre-generated audio");
|
||||||
|
return pregenerated;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +120,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;
|
||||||
|
|||||||
+100
-9
@@ -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,19 @@ 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.logging.Logger
|
||||||
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 +41,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 +66,56 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||||||
hasGenerate = _obj.has("generate");
|
hasGenerate = _obj.has("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var _pregenerate: V8Deferred<String?>? = null;
|
||||||
|
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||||
|
_pregenerate = generateAsync(scope);
|
||||||
|
return _pregenerate;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||||
|
if(!hasGenerate)
|
||||||
|
return V8Deferred(CompletableDeferred(manifest));
|
||||||
|
if(_obj.isClosed)
|
||||||
|
throw IllegalStateException("Source object already closed");
|
||||||
|
val pregenerated = _pregenerate;
|
||||||
|
if(pregenerated != null) {
|
||||||
|
Logger.w("JSDashManifestRawSource", "Returning pre-generated video");
|
||||||
|
return pregenerated;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +126,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 +175,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();
|
||||||
|
|||||||
+3
-1
@@ -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)
|
||||||
|
|||||||
+9
-2
@@ -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)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+39
-17
@@ -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);
|
||||||
|
|||||||
+2
@@ -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);
|
||||||
|
|||||||
+2
-1
@@ -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)
|
||||||
|
|||||||
+5
-2
@@ -11,7 +11,6 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
|||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
|
||||||
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.IHLSManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
@@ -19,7 +18,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -53,6 +52,10 @@ class LocalVideoDetails: IPlatformVideoDetails {
|
|||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
override val isShort: Boolean = false;
|
override val isShort: Boolean = false;
|
||||||
|
|
||||||
|
override var playbackTime: Long = -1;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override var playbackDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
constructor(file: File) {
|
constructor(file: File) {
|
||||||
id = PlatformID("Local", file.path, "LOCAL")
|
id = PlatformID("Local", file.path, "LOCAL")
|
||||||
name = file.name;
|
name = file.name;
|
||||||
|
|||||||
+5
-7
@@ -7,12 +7,12 @@ import java.util.stream.IntStream
|
|||||||
* A Content MultiPager that returns results based on a specified distribution
|
* A Content MultiPager that returns results based on a specified distribution
|
||||||
* TODO: Merge all basic distribution pagers
|
* TODO: Merge all basic distribution pagers
|
||||||
*/
|
*/
|
||||||
class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
class MultiDistributionContentPager<T : IPlatformContent> : MultiPager<T> {
|
||||||
|
|
||||||
private val dist : HashMap<IPager<IPlatformContent>, Float>;
|
private val dist : HashMap<IPager<T>, Float>;
|
||||||
private val distConsumed : HashMap<IPager<IPlatformContent>, Float>;
|
private val distConsumed : HashMap<IPager<T>, Float>;
|
||||||
|
|
||||||
constructor(pagers : Map<IPager<IPlatformContent>, Float>) : super(pagers.keys.toMutableList()) {
|
constructor(pagers : Map<IPager<T>, Float>, pageSize: Int = 9) : super(pagers.keys.toMutableList(), false, pageSize) {
|
||||||
val distTotal = pagers.values.sum();
|
val distTotal = pagers.values.sum();
|
||||||
dist = HashMap();
|
dist = HashMap();
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
override fun selectItemIndex(options: Array<SelectionOption<T>>): Int {
|
||||||
if(options.size == 0)
|
if(options.size == 0)
|
||||||
return -1;
|
return -1;
|
||||||
var bestIndex = 0;
|
var bestIndex = 0;
|
||||||
@@ -42,6 +42,4 @@ class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
|||||||
distConsumed[options[bestIndex].pager.getPager()] = bestConsumed;
|
distConsumed[options[bestIndex].pager.getPager()] = bestConsumed;
|
||||||
return bestIndex;
|
return bestIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
private val MAX_LAUNCH_RETRIES = 3
|
private val MAX_LAUNCH_RETRIES = 3
|
||||||
private var _lastLaunchTime_ms = 0L
|
private var _lastLaunchTime_ms = 0L
|
||||||
private var _retryJob: Job? = null
|
private var _retryJob: Job? = null
|
||||||
|
private var _autoLaunchEnabled = true
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -305,6 +306,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_autoLaunchEnabled = true
|
||||||
_started = true;
|
_started = true;
|
||||||
_sessionId = null;
|
_sessionId = null;
|
||||||
_launchRetries = 0
|
_launchRetries = 0
|
||||||
@@ -546,6 +548,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
if (appId == "CC1AD845") {
|
if (appId == "CC1AD845") {
|
||||||
sessionIsRunning = true;
|
sessionIsRunning = true;
|
||||||
|
_autoLaunchEnabled = false
|
||||||
|
|
||||||
if (_sessionId == null) {
|
if (_sessionId == null) {
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
connectionState = CastConnectionState.CONNECTED;
|
||||||
@@ -558,7 +561,6 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
_transportId = transportId;
|
_transportId = transportId;
|
||||||
|
|
||||||
requestMediaStatus();
|
requestMediaStatus();
|
||||||
playVideo();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,21 +570,22 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
if (System.currentTimeMillis() - _lastLaunchTime_ms > 5000) {
|
if (System.currentTimeMillis() - _lastLaunchTime_ms > 5000) {
|
||||||
_sessionId = null
|
_sessionId = null
|
||||||
_mediaSessionId = null
|
_mediaSessionId = null
|
||||||
setTime(0.0)
|
|
||||||
_transportId = null
|
_transportId = null
|
||||||
|
|
||||||
if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
if (_autoLaunchEnabled) {
|
||||||
Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}")
|
if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||||
_launchRetries++
|
Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}")
|
||||||
launchPlayer()
|
_launchRetries++
|
||||||
} else if (!_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
launchPlayer()
|
||||||
// Maybe the first GET_STATUS came back empty; still try launching
|
} else {
|
||||||
Logger.i(TAG, "Player not found; triggering launch #${_launchRetries + 1}")
|
// Maybe the first GET_STATUS came back empty; still try launching
|
||||||
_launching = true
|
Logger.i(TAG, "Player not found; triggering launch #${_launchRetries + 1}")
|
||||||
_launchRetries++
|
_launching = true
|
||||||
launchPlayer()
|
_launchRetries++
|
||||||
|
launchPlayer()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.e(TAG, "Player not found after $_launchRetries attempts; giving up.")
|
Logger.e(TAG, "Player not found ($_launchRetries, _autoLaunchEnabled = $_autoLaunchEnabled); giving up.")
|
||||||
Logger.i(TAG, "Unable to start media receiver on device")
|
Logger.i(TAG, "Unable to start media receiver on device")
|
||||||
stop()
|
stop()
|
||||||
}
|
}
|
||||||
@@ -599,6 +602,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
} else {
|
} else {
|
||||||
_launching = false
|
_launching = false
|
||||||
_launchRetries = 0
|
_launchRetries = 0
|
||||||
|
_autoLaunchEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val volume = status.getJSONObject("volume");
|
val volume = status.getJSONObject("volume");
|
||||||
@@ -636,10 +640,16 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
stopVideo();
|
stopVideo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val needsLoad = statuses.length() == 0 || (statuses.getJSONObject(0).getString("playerState") == "IDLE")
|
||||||
|
if (needsLoad && _contentId != null && _mediaSessionId == null) {
|
||||||
|
Logger.i(TAG, "Receiver idle, sending initial LOAD")
|
||||||
|
playVideo()
|
||||||
|
}
|
||||||
} else if (type == "CLOSE") {
|
} else if (type == "CLOSE") {
|
||||||
if (message.sourceId == "receiver-0") {
|
if (message.sourceId == "receiver-0") {
|
||||||
Logger.i(TAG, "Close received.");
|
Logger.i(TAG, "Close received.");
|
||||||
stop();
|
stopCasting();
|
||||||
} else if (_transportId == message.sourceId) {
|
} else if (_transportId == message.sourceId) {
|
||||||
throw Exception("Transport id closed.")
|
throw Exception("Transport id closed.")
|
||||||
}
|
}
|
||||||
@@ -676,6 +686,10 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
localAddress = null;
|
localAddress = null;
|
||||||
_started = false;
|
_started = false;
|
||||||
|
|
||||||
|
_contentId = null
|
||||||
|
_contentType = null
|
||||||
|
_streamType = null
|
||||||
|
|
||||||
_retryJob?.cancel()
|
_retryJob?.cancel()
|
||||||
_retryJob = null
|
_retryJob = null
|
||||||
|
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
headerBytesRead += read
|
headerBytesRead += read
|
||||||
}
|
}
|
||||||
|
|
||||||
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
|
val size = ((buffer[3].toUByte().toLong() shl 24) or (buffer[2].toUByte().toLong() shl 16) or (buffer[1].toUByte().toLong() shl 8) or buffer[0].toUByte().toLong()).toInt();
|
||||||
if (size > buffer.size) {
|
if (size > buffer.size) {
|
||||||
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
|
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.builders.DashBuilder
|
import com.futo.platformplayer.builders.DashBuilder
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
@@ -64,6 +65,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 +91,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 +435,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 = shouldProxyStreams(ad, videoSource, audioSource)
|
||||||
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 {
|
||||||
@@ -766,7 +752,7 @@ class StateCasting {
|
|||||||
|
|
||||||
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource)
|
||||||
|
|
||||||
val url = getLocalUrl(ad);
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
@@ -1129,9 +1115,14 @@ class StateCasting {
|
|||||||
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldProxyStreams(castingDevice: CastingDevice, videoSource: IVideoSource?, audioSource: IAudioSource?): Boolean {
|
||||||
|
val hasRequestModifier = (videoSource as? JSSource)?.hasRequestModifier == true || (audioSource as? JSSource)?.hasRequestModifier == true
|
||||||
|
return Settings.instance.casting.alwaysProxyRequests || castingDevice !is FCastCastingDevice || hasRequestModifier
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource)
|
||||||
|
|
||||||
val url = getLocalUrl(ad);
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
@@ -1236,7 +1227,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 +1274,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) {
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
||||||
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||||
|
|
||||||
private inline fun <reified T> createRemoteObjectArray(objs: Iterable<T>): List<V8RemoteObject> {
|
private inline fun <reified T> createRemoteObjectArray(objs: Iterable<T>): List<V8RemoteObject?> {
|
||||||
val remotes = mutableListOf<V8RemoteObject>();
|
val remotes = mutableListOf<V8RemoteObject?>();
|
||||||
for(obj in objs)
|
for(obj in objs)
|
||||||
remotes.add(createRemoteObject(obj)!!);
|
remotes.add(createRemoteObject(obj));
|
||||||
return remotes;
|
return remotes;
|
||||||
}
|
}
|
||||||
private inline fun <reified T> createRemoteObject(obj: T): V8RemoteObject? {
|
private inline fun <reified T> createRemoteObject(obj: T): V8RemoteObject? {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_buttonTutorial.setOnClickListener {
|
_buttonTutorial.setOnClickListener {
|
||||||
UIDialogs.showCastingTutorialDialog(context)
|
UIDialogs.showCastingTutorialDialog(context, ownerActivity)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
private fun performDismiss(shouldShowCastingDialog: Boolean = true) {
|
private fun performDismiss(shouldShowCastingDialog: Boolean = true) {
|
||||||
if (shouldShowCastingDialog) {
|
if (shouldShowCastingDialog) {
|
||||||
UIDialogs.showCastingDialog(context);
|
UIDialogs.showCastingDialog(context, ownerActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class CastingHelpDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
|
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
|
||||||
dismiss()
|
dismiss()
|
||||||
UIDialogs.showCastingAddDialog(context)
|
UIDialogs.showCastingAddDialog(context, ownerActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_buttonClose.setOnClickListener { dismiss(); };
|
_buttonClose.setOnClickListener { dismiss(); };
|
||||||
_buttonAdd.setOnClickListener {
|
_buttonAdd.setOnClickListener {
|
||||||
UIDialogs.showCastingAddDialog(context);
|
UIDialogs.showCastingAddDialog(context, ownerActivity);
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,9 +139,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
|
|
||||||
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -303,9 +303,10 @@ class VideoDownload {
|
|||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
|
val resolvedPlaylistUrl = playlistResponse.url
|
||||||
val playlistContent = playlistResponse.body?.string()
|
val playlistContent = playlistResponse.body?.string()
|
||||||
if (playlistContent != null) {
|
if (playlistContent != null) {
|
||||||
videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, source.url))
|
videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, resolvedPlaylistUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -351,9 +352,10 @@ class VideoDownload {
|
|||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
|
val resolvedPlaylistUrl = playlistResponse.url
|
||||||
val playlistContent = playlistResponse.body?.string()
|
val playlistContent = playlistResponse.body?.string()
|
||||||
if (playlistContent != null) {
|
if (playlistContent != null) {
|
||||||
audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, source.url))
|
audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, resolvedPlaylistUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -717,7 +719,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||||
|
|
||||||
var written = 0;
|
var written: Long = 0;
|
||||||
var indexCounter = 0;
|
var indexCounter = 0;
|
||||||
onProgress(foundCues.count().toLong(), 0, 0);
|
onProgress(foundCues.count().toLong(), 0, 0);
|
||||||
for(cue in foundCues) {
|
for(cue in foundCues) {
|
||||||
@@ -742,7 +744,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
indexCounter++;
|
indexCounter++;
|
||||||
}
|
}
|
||||||
sourceLength = written.toLong();
|
sourceLength = written;
|
||||||
|
|
||||||
Logger.i(TAG, "$name downloadSource Finished");
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||||||
|
|
||||||
override val isShort: Boolean get() = videoSerialized.isShort;
|
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||||
|
|
||||||
|
override var playbackTime: Long = -1;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override var playbackDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
//TODO: Offline subtitles
|
//TODO: Offline subtitles
|
||||||
override val subtitles: List<ISubtitleSource> = listOf();
|
override val subtitles: List<ISubtitleSource> = listOf();
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ class V8RemoteObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun List<V8RemoteObject>.serialize() : String {
|
fun List<V8RemoteObject?>.serialize() : String {
|
||||||
return _gson.toJson(this);
|
return _gson.toJson(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -174,7 +194,11 @@ class PackageBridge : V8Package {
|
|||||||
|
|
||||||
val stackTrace = Thread.currentThread().stackTrace;
|
val stackTrace = Thread.currentThread().stackTrace;
|
||||||
val callerMethod = stackTrace.findLast {
|
val callerMethod = stackTrace.findLast {
|
||||||
it.className == JSClient::class.java.name
|
it.className == JSClient::class.java.name &&
|
||||||
|
it.methodName != "isBusy" &&
|
||||||
|
it.methodName != "busy" &&
|
||||||
|
it.methodName != "getCopy" &&
|
||||||
|
it.methodName != "isBusyWith"
|
||||||
}?.methodName ?: "";
|
}?.methodName ?: "";
|
||||||
val session = StateApp.instance.sessionId;
|
val session = StateApp.instance.sessionId;
|
||||||
val pluginId = _plugin.config.id;
|
val pluginId = _plugin.config.id;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+7
-3
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.structures.IReplacerPager
|
|||||||
import com.futo.platformplayer.api.media.structures.MultiPager
|
import com.futo.platformplayer.api.media.structures.MultiPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
@@ -61,7 +62,7 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
|||||||
private var _query: String? = null
|
private var _query: String? = null
|
||||||
private var _searchView: SearchView? = null
|
private var _searchView: SearchView? = null
|
||||||
|
|
||||||
val onContentClicked = Event2<IPlatformContent, Long>();
|
val onContentClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>();
|
||||||
val onContentUrlClicked = Event2<String, ContentType>();
|
val onContentUrlClicked = Event2<String, ContentType>();
|
||||||
val onUrlClicked = Event1<String>();
|
val onUrlClicked = Event1<String>();
|
||||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||||
@@ -208,10 +209,13 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
|||||||
_searchView = searchView
|
_searchView = searchView
|
||||||
updateSearchViewVisibility()
|
updateSearchViewVisibility()
|
||||||
|
|
||||||
_adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
|
_adapterResults = PreviewContentListAdapter(lifecycleScope, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
|
||||||
this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
|
this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
|
||||||
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
|
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
|
||||||
this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit);
|
this.onContentClicked.subscribe { content, num ->
|
||||||
|
val results = ArrayList(_results)
|
||||||
|
this@ChannelContentsFragment.onContentClicked.emit(content, num, Pair(_pager!!, results))
|
||||||
|
}
|
||||||
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
||||||
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
||||||
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
||||||
|
|||||||
+1
-1
@@ -148,7 +148,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_recyclerResults = view.findViewById(R.id.recycler_videos)
|
_recyclerResults = view.findViewById(R.id.recycler_videos)
|
||||||
|
|
||||||
_adapterResults = PreviewContentListAdapter(
|
_adapterResults = PreviewContentListAdapter(
|
||||||
view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
|
lifecycleScope, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
|
||||||
).apply {
|
).apply {
|
||||||
this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit)
|
this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit)
|
||||||
this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit)
|
this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit)
|
||||||
|
|||||||
+4
-1
@@ -15,6 +15,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
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
|
||||||
@@ -375,6 +376,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
|
|
||||||
fun newInstance() = MenuBottomBarFragment().apply { }
|
fun newInstance() = MenuBottomBarFragment().apply { }
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
//Add configurable buttons here
|
//Add configurable buttons here
|
||||||
var buttonDefinitions = listOf(
|
var buttonDefinitions = listOf(
|
||||||
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
|
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
|
||||||
@@ -390,13 +392,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
|
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
|
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
|
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
|
||||||
|
ButtonDefinition(11, R.drawable.ic_smart_display, R.drawable.ic_smart_display_filled, R.string.shorts, canToggle = true, { it.currentMain is ShortsFragment && !(it.currentMain as ShortsFragment).isChannelShortsMode }, { it.navigate<ShortsFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>(withHistory = false) }),
|
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>(withHistory = false) }),
|
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>(withHistory = false) }),
|
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>(withHistory = false) }),
|
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>(withHistory = false) }),
|
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>(withHistory = false) }),
|
||||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
||||||
val c = it.context ?: return@ButtonDefinition;
|
val c = it.context ?: return@ButtonDefinition;
|
||||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||||
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
||||||
val intent = Intent(c, SettingsActivity::class.java);
|
val intent = Intent(c, SettingsActivity::class.java);
|
||||||
|
|||||||
+9
-1
@@ -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)
|
||||||
@@ -211,6 +211,14 @@ class ChannelFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
adapter.onShortClicked.subscribe { v, _, pagerPair ->
|
||||||
|
when (v) {
|
||||||
|
is IPlatformVideo -> {
|
||||||
|
StatePlayer.instance.clearQueue()
|
||||||
|
fragment.navigate<ShortsFragment>(Triple(v, pagerPair!!.first, pagerPair.second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
adapter.onAddToClicked.subscribe { content ->
|
adapter.onAddToClicked.subscribe { content ->
|
||||||
_overlayContainer.let {
|
_overlayContainer.let {
|
||||||
if (content is IPlatformVideo) _slideUpOverlay =
|
if (content is IPlatformVideo) _slideUpOverlay =
|
||||||
|
|||||||
+15
-3
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -19,6 +20,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||||||
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.models.JSWeb
|
import com.futo.platformplayer.api.media.platforms.js.models.JSWeb
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.ShortView.Companion
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
@@ -34,6 +36,9 @@ import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
|
|||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.futo.platformplayer.withTimestamp
|
import com.futo.platformplayer.withTimestamp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@@ -59,7 +64,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
||||||
_exoPlayer = player;
|
_exoPlayer = player;
|
||||||
|
|
||||||
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
||||||
attachAdapterEvents(this);
|
attachAdapterEvents(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,8 +251,15 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Is this still necessary?
|
//TODO: Is this still necessary?
|
||||||
if(viewHolder.childViewHolder is ContentPreviewViewHolder)
|
if(viewHolder.childViewHolder is ContentPreviewViewHolder) {
|
||||||
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "playPreview failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopVideo() {
|
private fun stopVideo() {
|
||||||
|
|||||||
@@ -0,0 +1,883 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
|
import android.view.animation.OvershootInterpolator
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.Format
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||||
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.special.CommentsModalBottomSheet
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
|
import com.futo.platformplayer.toHumanBytesSize
|
||||||
|
import com.futo.platformplayer.views.buttons.ShortsButton
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTitle
|
||||||
|
import com.futo.platformplayer.views.pills.OnLikeDislikeUpdatedArgs
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
import com.futo.platformplayer.views.video.FutoShortPlayer
|
||||||
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
||||||
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_AUDIO_CONTAINERS
|
||||||
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_VIDEO_CONTAINERS
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
|
import com.futo.polycentric.core.ContentType
|
||||||
|
import com.futo.polycentric.core.Models
|
||||||
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
//import com.google.android.material.button.MaterialButton
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import userpackage.Protocol
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class ShortView : FrameLayout {
|
||||||
|
private lateinit var fragment: MainFragment
|
||||||
|
private val player: FutoShortPlayer
|
||||||
|
|
||||||
|
private val channelInfo: LinearLayout
|
||||||
|
private val creatorThumbnail: CreatorThumbnail
|
||||||
|
private val channelName: TextView
|
||||||
|
private val videoTitle: TextView
|
||||||
|
private val videoSubtitle: TextView
|
||||||
|
private val platformIndicator: PlatformIndicator
|
||||||
|
|
||||||
|
//TODO: Replace with non-material button
|
||||||
|
private val backButton: MaterialButton
|
||||||
|
private val backButtonContainer: ConstraintLayout
|
||||||
|
|
||||||
|
private val likeButton: ShortsButton
|
||||||
|
//private val likeCount: TextView
|
||||||
|
private val dislikeButton: ShortsButton
|
||||||
|
//private val dislikeCount: TextView
|
||||||
|
|
||||||
|
private val commentsButton: ShortsButton
|
||||||
|
private val shareButton: ShortsButton
|
||||||
|
private val refreshButton: ShortsButton
|
||||||
|
private val qualityButton: ShortsButton
|
||||||
|
|
||||||
|
private val playPauseOverlay: FrameLayout
|
||||||
|
private val playPauseIcon: ImageView
|
||||||
|
|
||||||
|
private val overlayLoading: FrameLayout
|
||||||
|
private val overlayLoadingSpinner: ImageView
|
||||||
|
private lateinit var overlayQualityContainer: FrameLayout
|
||||||
|
|
||||||
|
private var overlayQualitySelector: SlideUpMenuOverlay? = null
|
||||||
|
|
||||||
|
private var video: IPlatformVideo? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
onVideoUpdated.emit(value)
|
||||||
|
}
|
||||||
|
private var videoDetails: IPlatformVideoDetails? = null
|
||||||
|
|
||||||
|
private var playWhenReady = false
|
||||||
|
|
||||||
|
private var _lastVideoSource: IVideoSource? = null
|
||||||
|
private var _lastAudioSource: IAudioSource? = null
|
||||||
|
private var _lastSubtitleSource: ISubtitleSource? = null
|
||||||
|
|
||||||
|
private var loadVideoTask: TaskHandler<String, IPlatformVideoDetails>? = null
|
||||||
|
private var loadLikesTask: TaskHandler<IPlatformVideo, Pair<Protocol.Reference, Protocol.QueryReferencesResponse>>? =
|
||||||
|
null
|
||||||
|
|
||||||
|
val onResetTriggered = Event0()
|
||||||
|
private val onPlayingToggled = Event1<Boolean>()
|
||||||
|
private val onLikesLoaded = Event3<RatingLikeDislikes, Boolean, Boolean>()
|
||||||
|
private val onLikeDislikeUpdated = Event1<OnLikeDislikeUpdatedArgs>()
|
||||||
|
private val onVideoUpdated = Event1<IPlatformVideo?>()
|
||||||
|
|
||||||
|
//TODO: Replace with non-material UI? Only true dependency on Material left
|
||||||
|
private val bottomSheet: CommentsModalBottomSheet = CommentsModalBottomSheet()
|
||||||
|
|
||||||
|
var likes: Long = 0
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
likeButton.withPrimaryText(value.toString());
|
||||||
|
//likeCount.text = value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
var dislikes: Long = 0
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
dislikeButton.withPrimaryText(value.toString());
|
||||||
|
//dislikeCount.text = value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(inflater: LayoutInflater, fragment: MainFragment, overlayQualityContainer: FrameLayout) : this(inflater.context) {
|
||||||
|
this.overlayQualityContainer = overlayQualityContainer
|
||||||
|
|
||||||
|
layoutParams = LayoutParams(
|
||||||
|
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
|
||||||
|
this.fragment = fragment
|
||||||
|
bottomSheet.mainFragment = fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required constructor for XML inflation
|
||||||
|
constructor(context: Context) : this(context, null, null)
|
||||||
|
|
||||||
|
// Required constructor for XML inflation with attributes
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, null)
|
||||||
|
|
||||||
|
// Required constructor for XML inflation with attributes and style
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int? = null) : super(
|
||||||
|
context, attrs, defStyleAttr ?: 0
|
||||||
|
) {
|
||||||
|
// Inflate the layout once here
|
||||||
|
inflate(context, R.layout.view_short, this)
|
||||||
|
|
||||||
|
// Initialize all val properties using findViewById
|
||||||
|
player = findViewById(R.id.short_player)
|
||||||
|
channelInfo = findViewById(R.id.channel_info)
|
||||||
|
creatorThumbnail = findViewById(R.id.creator_thumbnail)
|
||||||
|
channelName = findViewById(R.id.channel_name)
|
||||||
|
videoTitle = findViewById(R.id.video_title)
|
||||||
|
videoSubtitle = findViewById(R.id.video_subtitle)
|
||||||
|
platformIndicator = findViewById(R.id.short_platform_indicator)
|
||||||
|
backButton = findViewById(R.id.back_button)
|
||||||
|
backButtonContainer = findViewById(R.id.back_button_container)
|
||||||
|
likeButton = findViewById(R.id.like_button)
|
||||||
|
//likeCount = findViewById(R.id.like_count)
|
||||||
|
dislikeButton = findViewById(R.id.dislike_button)
|
||||||
|
//dislikeCount = findViewById(R.id.dislike_count)
|
||||||
|
commentsButton = findViewById(R.id.comments_button)
|
||||||
|
shareButton = findViewById(R.id.share_button)
|
||||||
|
refreshButton = findViewById(R.id.refresh_button)
|
||||||
|
qualityButton = findViewById(R.id.quality_button)
|
||||||
|
playPauseOverlay = findViewById(R.id.play_pause_overlay)
|
||||||
|
playPauseIcon = findViewById(R.id.play_pause_icon)
|
||||||
|
overlayLoading = findViewById(R.id.short_view_loading_overlay)
|
||||||
|
overlayLoadingSpinner = findViewById(R.id.short_view_loader)
|
||||||
|
|
||||||
|
player.setOnClickListener {
|
||||||
|
if (player.activelyPlaying) {
|
||||||
|
player.pause()
|
||||||
|
onPlayingToggled.emit(false)
|
||||||
|
} else {
|
||||||
|
player.play()
|
||||||
|
onPlayingToggled.emit(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPlayingToggled.subscribe { playing ->
|
||||||
|
if (playing) {
|
||||||
|
playPauseIcon.setImageResource(R.drawable.ic_play)
|
||||||
|
playPauseIcon.contentDescription = context.getString(R.string.play)
|
||||||
|
} else {
|
||||||
|
playPauseIcon.setImageResource(R.drawable.ic_pause)
|
||||||
|
playPauseIcon.contentDescription = context.getString(R.string.pause)
|
||||||
|
}
|
||||||
|
showPlayPauseIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
onVideoUpdated.subscribe {
|
||||||
|
Logger.i(TAG, "Shorts videoUpdated [${it?.name}] (isDetail: ${it is IPlatformVideoDetails}, thumbnail: ${it?.author?.thumbnail})");
|
||||||
|
videoTitle.text = it?.name
|
||||||
|
videoSubtitle.text = if(it is IPlatformVideoDetails) it?.description; else "";
|
||||||
|
platformIndicator.setPlatformFromClientID(it?.id?.pluginId)
|
||||||
|
creatorThumbnail.setThumbnail(it?.author?.thumbnail, true)
|
||||||
|
channelName.text = it?.author?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
backButton.setOnClickListener {
|
||||||
|
fragment.closeSegment()
|
||||||
|
}
|
||||||
|
|
||||||
|
channelInfo.setOnClickListener {
|
||||||
|
fragment.navigate<ChannelFragment>(video?.author)
|
||||||
|
}
|
||||||
|
|
||||||
|
videoTitle.setOnClickListener {
|
||||||
|
if (!bottomSheet.isAdded) {
|
||||||
|
bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commentsButton.onClick.subscribe {
|
||||||
|
if (!bottomSheet.isAdded) {
|
||||||
|
bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shareButton.onClick.subscribe {
|
||||||
|
val url = video?.shareUrl ?: video?.url
|
||||||
|
fragment.startActivity(Intent.createChooser(Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
putExtra(Intent.EXTRA_TEXT, url)
|
||||||
|
type = "text/plain"
|
||||||
|
}, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshButton.onClick.subscribe {
|
||||||
|
onResetTriggered.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshButton.setOnLongClickListener {
|
||||||
|
UIDialogs.toast(context, "Reload all platform shorts pagers")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
qualityButton.onClick.subscribe {
|
||||||
|
showVideoSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
likeButton.onClick.subscribe {
|
||||||
|
val checked = likeButton.iconId == R.drawable.ic_thumb_up_s // !likeButton.isChecked
|
||||||
|
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
|
||||||
|
if (checked) {
|
||||||
|
likes++
|
||||||
|
} else {
|
||||||
|
likes--
|
||||||
|
}
|
||||||
|
|
||||||
|
if(checked)
|
||||||
|
likeButton.withIcon(R.drawable.ic_thumb_up_s_filled) //.isChecked = checked
|
||||||
|
else
|
||||||
|
likeButton.withIcon(R.drawable.ic_thumb_up_s)
|
||||||
|
|
||||||
|
if (dislikeButton.iconId == R.drawable.ic_thumb_down_s_filled && checked) {
|
||||||
|
//dislikeButton.isChecked = false
|
||||||
|
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
|
||||||
|
dislikes--
|
||||||
|
}
|
||||||
|
|
||||||
|
onLikeDislikeUpdated.emit(
|
||||||
|
OnLikeDislikeUpdatedArgs(
|
||||||
|
it, likes, checked, dislikes, !checked
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dislikeButton.onClick.subscribe {
|
||||||
|
val checked = dislikeButton.iconId == R.drawable.ic_thumb_down_s //!dislikeButton.isChecked
|
||||||
|
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
|
||||||
|
if (checked) {
|
||||||
|
dislikes++
|
||||||
|
} else {
|
||||||
|
dislikes--
|
||||||
|
}
|
||||||
|
|
||||||
|
//dislikeButton.isChecked = checked
|
||||||
|
if(checked)
|
||||||
|
dislikeButton.withIcon(R.drawable.ic_thumb_down_s_filled) //.isChecked = checked
|
||||||
|
else
|
||||||
|
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
|
||||||
|
|
||||||
|
if (likeButton.iconId == R.drawable.ic_thumb_up_s_filled && checked) {
|
||||||
|
//likeButton.isChecked = false
|
||||||
|
likeButton.withIcon(R.drawable.ic_thumb_up_s);
|
||||||
|
likes--
|
||||||
|
}
|
||||||
|
|
||||||
|
onLikeDislikeUpdated.emit(
|
||||||
|
OnLikeDislikeUpdatedArgs(
|
||||||
|
it, likes, !checked, dislikes, checked
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLikesLoaded.subscribe(tag) { rating, liked, disliked ->
|
||||||
|
likes = rating.likes
|
||||||
|
dislikes = rating.dislikes
|
||||||
|
//likeButton.isChecked = liked
|
||||||
|
//dislikeButton.isChecked = disliked
|
||||||
|
|
||||||
|
dislikeButton.visibility = VISIBLE
|
||||||
|
likeButton.visibility = VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
player.onPlaybackStateChanged.subscribe {
|
||||||
|
val videoSource = _lastVideoSource
|
||||||
|
|
||||||
|
if (videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
|
||||||
|
val videoTracks =
|
||||||
|
player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO }
|
||||||
|
val audioTracks =
|
||||||
|
player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO }
|
||||||
|
|
||||||
|
val videoTrackFormats = mutableListOf<Format>()
|
||||||
|
val audioTrackFormats = mutableListOf<Format>()
|
||||||
|
|
||||||
|
if (videoTracks != null) {
|
||||||
|
for (i in 0 until videoTracks.mediaTrackGroup.length) videoTrackFormats.add(videoTracks.mediaTrackGroup.getFormat(i))
|
||||||
|
}
|
||||||
|
if (audioTracks != null) {
|
||||||
|
for (i in 0 until audioTracks.mediaTrackGroup.length) audioTrackFormats.add(audioTracks.mediaTrackGroup.getFormat(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQualitySourcesOverlay(videoDetails, null, videoTrackFormats.distinctBy { it.height }
|
||||||
|
.sortedBy { it.height }, audioTrackFormats.distinctBy { it.bitrate }
|
||||||
|
.sortedBy { it.bitrate })
|
||||||
|
} else {
|
||||||
|
updateQualitySourcesOverlay(videoDetails, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPlayPauseIcon() {
|
||||||
|
val overlay = playPauseOverlay
|
||||||
|
|
||||||
|
overlay.alpha = 0f
|
||||||
|
overlay.scaleX = 0f
|
||||||
|
overlay.scaleY = 0f
|
||||||
|
overlay.visibility = VISIBLE
|
||||||
|
|
||||||
|
overlay.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(400)
|
||||||
|
.setInterpolator(OvershootInterpolator(1.2f)).start()
|
||||||
|
|
||||||
|
overlay.postDelayed({
|
||||||
|
hidePlayPauseIcon()
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hidePlayPauseIcon() {
|
||||||
|
val overlay = playPauseOverlay
|
||||||
|
|
||||||
|
overlay.animate().alpha(0f).scaleX(0.8f).scaleY(0.8f).setDuration(300)
|
||||||
|
.setInterpolator(AccelerateInterpolator()).withEndAction {
|
||||||
|
overlay.visibility = GONE
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO merge this with the updateQualitySourcesOverlay for the normal video player
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
|
private fun updateQualitySourcesOverlay(videoDetails: IPlatformVideoDetails?, videoLocal: VideoLocal? = null, liveStreamVideoFormats: List<Format>? = null, liveStreamAudioFormats: List<Format>? = null) {
|
||||||
|
Logger.i(TAG, "updateQualitySourcesOverlay")
|
||||||
|
|
||||||
|
val video: IPlatformVideoDetails?
|
||||||
|
val localVideoSources: List<LocalVideoSource>?
|
||||||
|
val localAudioSource: List<LocalAudioSource>?
|
||||||
|
val localSubtitleSources: List<LocalSubtitleSource>?
|
||||||
|
|
||||||
|
val videoSources: List<IVideoSource>?
|
||||||
|
val audioSources: List<IAudioSource>?
|
||||||
|
|
||||||
|
if (videoDetails is VideoLocal) {
|
||||||
|
video = videoLocal?.videoSerialized
|
||||||
|
localVideoSources = videoDetails.videoSource.toList()
|
||||||
|
localAudioSource = videoDetails.audioSource.toList()
|
||||||
|
localSubtitleSources = videoDetails.subtitlesSources.toList()
|
||||||
|
videoSources = null
|
||||||
|
audioSources = null
|
||||||
|
} else {
|
||||||
|
video = videoDetails
|
||||||
|
videoSources = video?.video?.videoSources?.toList()
|
||||||
|
audioSources =
|
||||||
|
if (video?.video?.isUnMuxed == true) (video.video as VideoUnMuxedSourceDescriptor).audioSources.toList()
|
||||||
|
else null
|
||||||
|
if (videoLocal != null) {
|
||||||
|
localVideoSources = videoLocal.videoSource.toList()
|
||||||
|
localAudioSource = videoLocal.audioSource.toList()
|
||||||
|
localSubtitleSources = videoLocal.subtitlesSources.toList()
|
||||||
|
} else {
|
||||||
|
localVideoSources = null
|
||||||
|
localAudioSource = null
|
||||||
|
localSubtitleSources = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val doDedup = Settings.instance.playback.simplifySources
|
||||||
|
|
||||||
|
val bestVideoSources = if (doDedup) (videoSources?.map { it.height * it.width }?.distinct()
|
||||||
|
?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) }
|
||||||
|
?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource }))?.distinct()
|
||||||
|
?.filterNotNull()?.toList() ?: listOf() else videoSources?.toList() ?: listOf()
|
||||||
|
val bestAudioContainer =
|
||||||
|
audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container }
|
||||||
|
val bestAudioSources =
|
||||||
|
if (doDedup) audioSources?.filter { it.container == bestAudioContainer }
|
||||||
|
?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource })
|
||||||
|
?.distinct()?.toList() ?: listOf() else audioSources?.toList() ?: listOf()
|
||||||
|
|
||||||
|
val canSetSpeed = true
|
||||||
|
val currentPlaybackRate = player.getPlaybackRate()
|
||||||
|
overlayQualitySelector =
|
||||||
|
SlideUpMenuOverlay(
|
||||||
|
this.context, overlayQualityContainer, context.getString(
|
||||||
|
R.string.quality
|
||||||
|
), null, true, if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null, if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
|
||||||
|
setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate.toString())
|
||||||
|
onClick.subscribe { v ->
|
||||||
|
|
||||||
|
player.setPlaybackRate(v.toFloat())
|
||||||
|
setSelected(v)
|
||||||
|
|
||||||
|
}
|
||||||
|
} else null, if (localVideoSources?.isNotEmpty() == true) SlideUpMenuGroup(
|
||||||
|
this.context, context.getString(R.string.offline_video), "video", *localVideoSources.map {
|
||||||
|
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", tag = it, call = { handleSelectVideoTrack(it) })
|
||||||
|
}.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
else null, if (localAudioSource?.isNotEmpty() == true) SlideUpMenuGroup(
|
||||||
|
this.context, context.getString(R.string.offline_audio), "audio", *localAudioSource.map {
|
||||||
|
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), tag = it, call = { handleSelectAudioTrack(it) })
|
||||||
|
}.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
else null, if (localSubtitleSources?.isNotEmpty() == true) SlideUpMenuGroup(
|
||||||
|
this.context, context.getString(R.string.offline_subtitles), "subtitles", *localSubtitleSources.map {
|
||||||
|
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) })
|
||||||
|
}.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
else null, if (liveStreamVideoFormats?.isEmpty() == false) SlideUpMenuGroup(
|
||||||
|
this.context, context.getString(R.string.stream_video), "video", (listOf(
|
||||||
|
SlideUpMenuItem(this.context, R.drawable.ic_movie, "Auto", tag = "auto", call = { player.selectVideoTrack(-1) })
|
||||||
|
) + (liveStreamVideoFormats.map {
|
||||||
|
SlideUpMenuItem(
|
||||||
|
this.context, R.drawable.ic_movie, it.label ?: it.containerMimeType
|
||||||
|
?: it.bitrate.toString(), "${it.width}x${it.height}", tag = it, call = { player.selectVideoTrack(it.height) })
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
else null, if (liveStreamAudioFormats?.isEmpty() == false) SlideUpMenuGroup(
|
||||||
|
this.context, context.getString(R.string.stream_audio), "audio", *liveStreamAudioFormats.map {
|
||||||
|
SlideUpMenuItem(this.context, R.drawable.ic_music, "${it.label ?: it.containerMimeType} ${it.bitrate}", "", tag = it, call = { player.selectAudioTrack(it.bitrate) })
|
||||||
|
}.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
else null, if (bestVideoSources.isNotEmpty()) SlideUpMenuGroup(
|
||||||
|
this.context, context.getString(R.string.video), "video", *bestVideoSources.map {
|
||||||
|
val estSize = VideoHelper.estimateSourceSize(it)
|
||||||
|
val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""
|
||||||
|
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectVideoTrack(it) })
|
||||||
|
}.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
else null, if (bestAudioSources.isNotEmpty()) SlideUpMenuGroup(
|
||||||
|
this.context, context.getString(R.string.audio), "audio", *bestAudioSources.map {
|
||||||
|
val estSize = VideoHelper.estimateSourceSize(it)
|
||||||
|
val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""
|
||||||
|
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) })
|
||||||
|
}.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
else null, if (video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup(
|
||||||
|
this.context, context.getString(R.string.subtitles), "subtitles", *video.subtitles.map {
|
||||||
|
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) })
|
||||||
|
}.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSelectVideoTrack(videoSource: IVideoSource) {
|
||||||
|
Logger.i(TAG, "handleSelectAudioTrack(videoSource=$videoSource)")
|
||||||
|
if (_lastVideoSource == videoSource) return
|
||||||
|
|
||||||
|
_lastVideoSource = videoSource
|
||||||
|
|
||||||
|
playVideo(player.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSelectAudioTrack(audioSource: IAudioSource) {
|
||||||
|
Logger.i(TAG, "handleSelectAudioTrack(audioSource=$audioSource)")
|
||||||
|
if (_lastAudioSource == audioSource) return
|
||||||
|
|
||||||
|
_lastAudioSource = audioSource
|
||||||
|
|
||||||
|
playVideo(player.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSelectSubtitleTrack(subtitleSource: ISubtitleSource) {
|
||||||
|
Logger.i(TAG, "handleSelectSubtitleTrack(subtitleSource=$subtitleSource)")
|
||||||
|
var toSet: ISubtitleSource? = subtitleSource
|
||||||
|
if (_lastSubtitleSource == subtitleSource) toSet = null
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
player.swapSubtitles(toSet)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "handleSelectSubtitleTrack failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastSubtitleSource = toSet
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showVideoSettings() {
|
||||||
|
Logger.i(TAG, "showVideoSettings")
|
||||||
|
|
||||||
|
overlayQualitySelector?.selectOption("video", _lastVideoSource)
|
||||||
|
overlayQualitySelector?.selectOption("audio", _lastAudioSource)
|
||||||
|
overlayQualitySelector?.selectOption("subtitles", _lastSubtitleSource)
|
||||||
|
|
||||||
|
if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource) {
|
||||||
|
val videoTracks =
|
||||||
|
player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO }
|
||||||
|
|
||||||
|
var selectedQuality: Format? = null
|
||||||
|
|
||||||
|
if (videoTracks != null) {
|
||||||
|
for (i in 0 until videoTracks.mediaTrackGroup.length) {
|
||||||
|
if (videoTracks.mediaTrackGroup.getFormat(i).height == player.targetTrackVideoHeight) {
|
||||||
|
selectedQuality = videoTracks.mediaTrackGroup.getFormat(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var videoMenuGroup: SlideUpMenuGroup? = null
|
||||||
|
for (view in overlayQualitySelector!!.groupItems) {
|
||||||
|
if (view is SlideUpMenuGroup && view.groupTag == "video") {
|
||||||
|
videoMenuGroup = view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedQuality != null) {
|
||||||
|
videoMenuGroup?.getItem("auto")?.setSubText("")
|
||||||
|
overlayQualitySelector?.selectOption("video", selectedQuality)
|
||||||
|
} else {
|
||||||
|
videoMenuGroup?.getItem("auto")
|
||||||
|
?.setSubText("${player.exoPlayer?.player?.videoFormat?.width}x${player.exoPlayer?.player?.videoFormat?.height}")
|
||||||
|
overlayQualitySelector?.selectOption("video", "auto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentPlaybackRate = player.getPlaybackRate()
|
||||||
|
overlayQualitySelector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }
|
||||||
|
?.let {
|
||||||
|
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayQualitySelector?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) {
|
||||||
|
this.fragment = fragment
|
||||||
|
this.bottomSheet.mainFragment = fragment
|
||||||
|
this.overlayQualityContainer = overlayQualityContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeVideo(video: IPlatformVideo, isChannelShortsMode: Boolean) {
|
||||||
|
if (this.video?.url == video.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.video = video
|
||||||
|
|
||||||
|
refreshButton.visibility = if (isChannelShortsMode) {
|
||||||
|
GONE
|
||||||
|
} else {
|
||||||
|
GONE //TODO: Revert?
|
||||||
|
}
|
||||||
|
backButtonContainer.visibility = if (isChannelShortsMode) {
|
||||||
|
VISIBLE
|
||||||
|
} else {
|
||||||
|
GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
loadVideo(video.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun changeVideo(videoDetails: IPlatformVideoDetails) {
|
||||||
|
if (video?.url == videoDetails.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.video = videoDetails
|
||||||
|
this.videoDetails = videoDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
fun play() {
|
||||||
|
loadLikes(this.video!!)
|
||||||
|
player.clear()
|
||||||
|
player.attach()
|
||||||
|
player.clear()
|
||||||
|
playVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pause() {
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
playWhenReady = false
|
||||||
|
|
||||||
|
player.clear()
|
||||||
|
player.detach()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
loadVideoTask?.cancel()
|
||||||
|
loadLikesTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
if (isLoading) {
|
||||||
|
(overlayLoadingSpinner.drawable as Animatable?)?.start()
|
||||||
|
overlayLoading.visibility = VISIBLE
|
||||||
|
} else {
|
||||||
|
overlayLoading.visibility = GONE
|
||||||
|
(overlayLoadingSpinner.drawable as Animatable?)?.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLikes(video: IPlatformVideo) {
|
||||||
|
likeButton.visibility = GONE
|
||||||
|
dislikeButton.visibility = GONE
|
||||||
|
|
||||||
|
loadLikesTask?.cancel()
|
||||||
|
loadLikesTask =
|
||||||
|
TaskHandler<IPlatformVideo, Pair<Protocol.Reference, Protocol.QueryReferencesResponse>>(
|
||||||
|
StateApp.instance.scopeGetter, {
|
||||||
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
|
val extraBytesRef =
|
||||||
|
video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
|
|
||||||
|
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
||||||
|
ApiMethods.SERVER, ref, null, null, arrayListOf(
|
||||||
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||||
|
.setFromType(ContentType.OPINION.value).setValue(
|
||||||
|
ByteString.copyFrom(Opinion.like.data)
|
||||||
|
)
|
||||||
|
.build(), Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||||
|
.setFromType(ContentType.OPINION.value).setValue(
|
||||||
|
ByteString.copyFrom(Opinion.dislike.data)
|
||||||
|
).build()
|
||||||
|
), extraByteReferences = listOfNotNull(extraBytesRef)
|
||||||
|
)
|
||||||
|
|
||||||
|
Pair(ref, queryReferencesResponse)
|
||||||
|
}).success { (ref, queryReferencesResponse) ->
|
||||||
|
val likes = queryReferencesResponse.countsList[0]
|
||||||
|
val dislikes = queryReferencesResponse.countsList[1]
|
||||||
|
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())
|
||||||
|
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())
|
||||||
|
onLikesLoaded.emit(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked)
|
||||||
|
onLikeDislikeUpdated.subscribe(this) { args ->
|
||||||
|
if (args.hasLiked) {
|
||||||
|
args.processHandle.opinion(ref, Opinion.like)
|
||||||
|
} else if (args.hasDisliked) {
|
||||||
|
args.processHandle.opinion(ref, Opinion.dislike)
|
||||||
|
} else {
|
||||||
|
args.processHandle.opinion(ref, Opinion.neutral)
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
Logger.i(TAG, "Started backfill")
|
||||||
|
args.processHandle.fullyBackfillServersAnnounceExceptions()
|
||||||
|
Logger.i(TAG, "Finished backfill")
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatePolycentric.instance.updateLikeMap(
|
||||||
|
ref, args.hasLiked, args.hasDisliked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLikesTask?.run(video)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadVideo(url: String) {
|
||||||
|
loadVideoTask?.cancel()
|
||||||
|
videoDetails = null
|
||||||
|
_lastVideoSource = null
|
||||||
|
_lastAudioSource = null
|
||||||
|
_lastSubtitleSource = null
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
Logger.i(TAG, "Shorts loadVideo [${url}]");
|
||||||
|
val timeLoadVideoStart = System.currentTimeMillis();
|
||||||
|
loadVideoTask = TaskHandler<String, IPlatformVideoDetails>(
|
||||||
|
StateApp.instance.scopeGetter, {
|
||||||
|
val result = StatePlatform.instance.getContentDetails(it).await()
|
||||||
|
if (result !is IPlatformVideoDetails) throw IllegalStateException("Expected media content, found ${result.contentType}")
|
||||||
|
return@TaskHandler result
|
||||||
|
}).success { result ->
|
||||||
|
val timeLoadVideo = System.currentTimeMillis() - timeLoadVideoStart;
|
||||||
|
Logger.i(TAG, "Shorts loadVideo [${url}] took ${timeLoadVideo}ms");
|
||||||
|
videoDetails = result
|
||||||
|
video = result
|
||||||
|
|
||||||
|
if(Settings.instance.playback.shortsPregenerate)
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
if(result != null) {
|
||||||
|
val prefVid = VideoHelper.selectBestVideoSource(result.video, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||||
|
val prefAud = VideoHelper.selectBestAudioSource(result.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||||
|
|
||||||
|
if(prefVid != null && prefVid is JSDashManifestRawSource) {
|
||||||
|
Logger.i(TAG, "Shorts pregenerating video (${result.name})");
|
||||||
|
prefVid.pregenerateAsync(fragment.lifecycleScope);
|
||||||
|
}
|
||||||
|
if(prefAud != null && prefAud is JSDashManifestRawAudioSource) {
|
||||||
|
Logger.i(TAG, "Shorts pregenerating audio (${result.name})");
|
||||||
|
prefAud.pregenerateAsync(fragment.lifecycleScope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheet.video = result
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
if (playWhenReady) playVideo()
|
||||||
|
}.exception<NoPlatformClientException> {
|
||||||
|
Logger.w(TAG, "exception<NoPlatformClientException>", it)
|
||||||
|
UIDialogs.showDialog(
|
||||||
|
context, R.drawable.ic_sources, "No source enabled to support this video\n(${url})", null, null, 0, UIDialogs.Action("Close", { }, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
)
|
||||||
|
}.exception<ScriptLoginRequiredException> { e ->
|
||||||
|
Logger.w(TAG, "exception<ScriptLoginRequiredException>", e)
|
||||||
|
UIDialogs.showDialog(
|
||||||
|
context, R.drawable.ic_security, "Authentication", e.message, null, 0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Login", {
|
||||||
|
val id = e.config.let { if (it is SourcePluginConfig) it.id else null }
|
||||||
|
val didLogin =
|
||||||
|
if (id == null) false else StatePlugins.instance.loginPlugin(context, id) {
|
||||||
|
loadVideo(url)
|
||||||
|
}
|
||||||
|
if (!didLogin) UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login")
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
)
|
||||||
|
}.exception<ContentNotAvailableYetException> {
|
||||||
|
Logger.w(TAG, "exception<ContentNotAvailableYetException>", it)
|
||||||
|
UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${it.availableWhen}.", "Close") { }
|
||||||
|
}.exception<ScriptImplementationException> {
|
||||||
|
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||||
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, { loadVideo(url) }, null, fragment)
|
||||||
|
}.exception<ScriptAgeException> {
|
||||||
|
Logger.w(TAG, "exception<ScriptAgeException>", it)
|
||||||
|
UIDialogs.showDialog(
|
||||||
|
context, R.drawable.ic_lock, "Age restricted video", it.message, null, 0, UIDialogs.Action("Close", { }, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
)
|
||||||
|
}.exception<ScriptUnavailableException> {
|
||||||
|
Logger.w(TAG, "exception<ScriptUnavailableException>", it)
|
||||||
|
UIDialogs.showDialog(
|
||||||
|
context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), context.getString(R.string.this_video_is_unavailable), null, 0, UIDialogs.Action(context.getString(R.string.close), { }, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
)
|
||||||
|
}.exception<ScriptException> {
|
||||||
|
Logger.w(TAG, "exception<ScriptException>", it)
|
||||||
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, { loadVideo(url) }, null, fragment)
|
||||||
|
}.exception<Throwable> {
|
||||||
|
Logger.w(ChannelFragment.TAG, "Failed to load video.", it)
|
||||||
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadVideoTask?.run(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playVideo(resumePositionMs: Long = 0) {
|
||||||
|
val videoDetails = this@ShortView.videoDetails
|
||||||
|
|
||||||
|
if (videoDetails === null) {
|
||||||
|
playWhenReady = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQualitySourcesOverlay(videoDetails, null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val videoSource = _lastVideoSource
|
||||||
|
?: player.getPreferredVideoSource(videoDetails, Settings.instance.playback.getCurrentPreferredQualityPixelCount())
|
||||||
|
val audioSource = _lastAudioSource
|
||||||
|
?: player.getPreferredAudioSource(videoDetails, Settings.instance.playback.getPrimaryLanguage(context))
|
||||||
|
val subtitleSource = _lastSubtitleSource
|
||||||
|
?: (if (videoDetails is VideoLocal) videoDetails.subtitlesSources.firstOrNull() else null)
|
||||||
|
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
||||||
|
|
||||||
|
if (videoSource == null && audioSource == null) {
|
||||||
|
UIDialogs.showDialog(
|
||||||
|
context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), context.getString(R.string.this_video_is_unavailable), null, 0, UIDialogs.Action(context.getString(R.string.close), { }, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
)
|
||||||
|
StatePlatform.instance.clearContentDetailCache(videoDetails.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnail = videoDetails.thumbnails.getHQThumbnail()
|
||||||
|
/*
|
||||||
|
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
|
||||||
|
.load(thumbnail).into(object : CustomTarget<Bitmap>() {
|
||||||
|
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||||
|
player.setArtwork(resource.toDrawable(resources))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadCleared(placeholder: Drawable?) {
|
||||||
|
player.setArtwork(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else player.setArtwork(null)
|
||||||
|
*/
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
|
||||||
|
if (subtitleSource != null) player.swapSubtitles(subtitleSource)
|
||||||
|
player.seekTo(resumePositionMs)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "playVideo failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastVideoSource = videoSource
|
||||||
|
_lastAudioSource = audioSource
|
||||||
|
_lastSubtitleSource = subtitleSource
|
||||||
|
} catch (ex: UnsupportedCastException) {
|
||||||
|
Logger.e(TAG, "Failed to load cast media", ex)
|
||||||
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.unsupported_cast_format), ex)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to load media", ex)
|
||||||
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "VideoDetailView"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+370
@@ -0,0 +1,370 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.SoundEffectConstants
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class ShortsFragment : MainFragment() {
|
||||||
|
override val isMainView: Boolean = true
|
||||||
|
override val isTab: Boolean = true
|
||||||
|
override val hasBottomBar: Boolean get() = true
|
||||||
|
|
||||||
|
private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null
|
||||||
|
private var nextPageTask: TaskHandler<ShortsFragment, List<IPlatformVideo>>? = null
|
||||||
|
|
||||||
|
//TODO: Reduce number of pagers (1, or at most 2)
|
||||||
|
private var mainShortsPager: IPager<IPlatformVideo>? = null
|
||||||
|
private val mainShorts: MutableList<IPlatformVideo> = mutableListOf()
|
||||||
|
|
||||||
|
// the pager to call next on
|
||||||
|
private var currentShortsPager: IPager<IPlatformVideo>? = null
|
||||||
|
|
||||||
|
// the shorts array bound to the ViewPager2 adapter
|
||||||
|
private val currentShorts: MutableList<IPlatformVideo> = mutableListOf()
|
||||||
|
|
||||||
|
private var channelShortsPager: IPager<IPlatformVideo>? = null
|
||||||
|
private val channelShorts: MutableList<IPlatformVideo> = mutableListOf()
|
||||||
|
val isChannelShortsMode: Boolean
|
||||||
|
get() = channelShortsPager != null
|
||||||
|
|
||||||
|
private var viewPager: ViewPager2? = null
|
||||||
|
private lateinit var zeroState: LinearLayout
|
||||||
|
private lateinit var sourcesButton: BigButton
|
||||||
|
private lateinit var overlayLoading: FrameLayout
|
||||||
|
private lateinit var overlayLoadingSpinner: ImageView
|
||||||
|
private lateinit var overlayQualityContainer: FrameLayout
|
||||||
|
private var customViewAdapter: CustomViewAdapter? = null
|
||||||
|
|
||||||
|
// we just completely reset the data structure so we want to tell the adapter that
|
||||||
|
//TODO: Move most of this logic to ShortsView
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
(activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||||
|
super.onShownWithView(parameter, isBack)
|
||||||
|
|
||||||
|
if (parameter is Triple<*, *, *>) {
|
||||||
|
setLoading(false)
|
||||||
|
channelShorts.clear()
|
||||||
|
@Suppress("UNCHECKED_CAST") // TODO replace with a strongly typed parameter
|
||||||
|
channelShorts.addAll(parameter.third as ArrayList<IPlatformVideo>)
|
||||||
|
@Suppress("UNCHECKED_CAST") // TODO replace with a strongly typed parameter
|
||||||
|
channelShortsPager = parameter.second as IPager<IPlatformVideo>
|
||||||
|
|
||||||
|
currentShorts.clear()
|
||||||
|
currentShorts.addAll(channelShorts)
|
||||||
|
currentShortsPager = channelShortsPager
|
||||||
|
|
||||||
|
viewPager?.adapter?.notifyDataSetChanged()
|
||||||
|
|
||||||
|
viewPager?.post {
|
||||||
|
viewPager?.currentItem = channelShorts.indexOfFirst {
|
||||||
|
return@indexOfFirst (parameter.first as IPlatformVideo).id == it.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isChannelShortsMode) {
|
||||||
|
channelShortsPager = null
|
||||||
|
channelShorts.clear()
|
||||||
|
currentShorts.clear()
|
||||||
|
|
||||||
|
if (loadPagerTask == null) {
|
||||||
|
currentShorts.addAll(mainShorts)
|
||||||
|
currentShortsPager = mainShortsPager
|
||||||
|
} else {
|
||||||
|
setLoading(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPager?.adapter?.notifyDataSetChanged()
|
||||||
|
viewPager?.currentItem = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
updateZeroState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateMainView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_shorts, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
viewPager = view.findViewById(R.id.view_pager)
|
||||||
|
zeroState = view.findViewById(R.id.zero_state)
|
||||||
|
sourcesButton = view.findViewById(R.id.sources_button)
|
||||||
|
overlayLoading = view.findViewById(R.id.short_view_loading_overlay)
|
||||||
|
overlayLoadingSpinner = view.findViewById(R.id.short_view_loader)
|
||||||
|
overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview)
|
||||||
|
|
||||||
|
sourcesButton.onClick.subscribe {
|
||||||
|
navigate<SourcesFragment>()
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
Logger.i(TAG, "Creating adapter")
|
||||||
|
val customViewAdapter =
|
||||||
|
CustomViewAdapter(currentShorts, layoutInflater, this@ShortsFragment, overlayQualityContainer, { isChannelShortsMode }) {
|
||||||
|
if (!currentShortsPager!!.hasMorePages()) {
|
||||||
|
return@CustomViewAdapter
|
||||||
|
}
|
||||||
|
nextPage()
|
||||||
|
}
|
||||||
|
customViewAdapter.onResetTriggered.subscribe {
|
||||||
|
setLoading(true)
|
||||||
|
loadPager()
|
||||||
|
|
||||||
|
loadPagerTask!!.success {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val viewPager = viewPager!!
|
||||||
|
viewPager.adapter = customViewAdapter
|
||||||
|
|
||||||
|
this.customViewAdapter = customViewAdapter
|
||||||
|
|
||||||
|
if (loadPagerTask == null) {// && currentShorts.isEmpty()) {
|
||||||
|
loadPager()
|
||||||
|
|
||||||
|
loadPagerTask!!.success {
|
||||||
|
setLoading(false)
|
||||||
|
updateZeroState()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLoading(false)
|
||||||
|
updateZeroState()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||||
|
fun play(adapter: CustomViewAdapter, position: Int) {
|
||||||
|
val recycler = (viewPager.getChildAt(0) as RecyclerView)
|
||||||
|
val viewHolder =
|
||||||
|
recycler.findViewHolderForAdapterPosition(position) as CustomViewHolder?
|
||||||
|
|
||||||
|
if (viewHolder == null) {
|
||||||
|
adapter.needToPlay = position
|
||||||
|
} else {
|
||||||
|
val focusedView = viewHolder.shortView
|
||||||
|
focusedView.play()
|
||||||
|
adapter.previousShownView = focusedView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
val adapter = (viewPager.adapter as CustomViewAdapter)
|
||||||
|
if (adapter.previousShownView == null) {
|
||||||
|
// play if this page selection didn't trigger by a swipe from another page
|
||||||
|
play(adapter, position)
|
||||||
|
} else {
|
||||||
|
adapter.previousShownView?.stop()
|
||||||
|
adapter.previousShownView = null
|
||||||
|
adapter.newPosition = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the state to idle to prevent UI lag
|
||||||
|
override fun onPageScrollStateChanged(state: Int) {
|
||||||
|
super.onPageScrollStateChanged(state)
|
||||||
|
if (state == ViewPager2.SCROLL_STATE_IDLE) {
|
||||||
|
val adapter = (viewPager.adapter as CustomViewAdapter)
|
||||||
|
val position = adapter.newPosition ?: return
|
||||||
|
adapter.newPosition = null
|
||||||
|
|
||||||
|
play(adapter, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateZeroState() {
|
||||||
|
if (mainShorts.isEmpty() && !isChannelShortsMode && loadPagerTask == null) {
|
||||||
|
zeroState.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
zeroState.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextPage() {
|
||||||
|
Logger.i(TAG, "ShortsFragment nextPage");
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
currentShortsPager!!.nextPage();
|
||||||
|
}
|
||||||
|
val newVideos = currentShortsPager!!.getResults();
|
||||||
|
val prevCount = customViewAdapter!!.itemCount
|
||||||
|
Logger.i(TAG, "Shorts nextPage took ${time}ms, ${prevCount}-${prevCount + newVideos.size}, hasMore: ${currentShortsPager?.hasMorePages()}");
|
||||||
|
currentShorts.addAll(newVideos)
|
||||||
|
if (isChannelShortsMode) {
|
||||||
|
channelShorts.addAll(newVideos)
|
||||||
|
} else {
|
||||||
|
mainShorts.addAll(newVideos)
|
||||||
|
}
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
|
||||||
|
}
|
||||||
|
nextPageTask = null
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Shorts Failed to call nextPage", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we just completely reset the data structure so we want to tell the adapter that
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
private fun loadPager() {
|
||||||
|
loadPagerTask?.cancel()
|
||||||
|
|
||||||
|
Logger.i(TAG, "Shorts loadPage");
|
||||||
|
var loadPageStart = System.currentTimeMillis();
|
||||||
|
val loadPagerTask =
|
||||||
|
TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, {
|
||||||
|
val pager = StatePlatform.instance.getShorts();
|
||||||
|
|
||||||
|
return@TaskHandler pager
|
||||||
|
}).success { pager ->
|
||||||
|
val timeLoadPage = System.currentTimeMillis() - loadPageStart;
|
||||||
|
Logger.i(TAG, "Shorts loadPage took ${timeLoadPage}ms");
|
||||||
|
mainShorts.clear()
|
||||||
|
mainShorts.addAll(pager.getResults())
|
||||||
|
mainShortsPager = pager
|
||||||
|
|
||||||
|
if (!isChannelShortsMode) {
|
||||||
|
currentShorts.clear()
|
||||||
|
currentShorts.addAll(mainShorts)
|
||||||
|
currentShortsPager = pager
|
||||||
|
|
||||||
|
// if the view pager exists go back to the beginning
|
||||||
|
viewPager?.adapter?.notifyDataSetChanged()
|
||||||
|
viewPager?.currentItem = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPagerTask = null
|
||||||
|
}.exception<Throwable> { err ->
|
||||||
|
val message = "Unable to load shorts $err"
|
||||||
|
Logger.w(TAG, message, err)
|
||||||
|
if (context != null) {
|
||||||
|
UIDialogs.showDialog(
|
||||||
|
requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action(
|
||||||
|
"Close", { }, UIDialogs.ActionStyle.PRIMARY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@exception
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadPagerTask = loadPagerTask
|
||||||
|
|
||||||
|
loadPagerTask.run(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
if (isLoading) {
|
||||||
|
(overlayLoadingSpinner.drawable as Animatable?)?.start()
|
||||||
|
overlayLoading.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
overlayLoading.visibility = View.GONE
|
||||||
|
(overlayLoadingSpinner.drawable as Animatable?)?.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
customViewAdapter?.previousShownView?.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
loadPagerTask?.cancel()
|
||||||
|
loadPagerTask = null
|
||||||
|
nextPageTask?.cancel()
|
||||||
|
nextPageTask = null
|
||||||
|
customViewAdapter?.previousShownView?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ShortsFragment"
|
||||||
|
|
||||||
|
fun newInstance() = ShortsFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomViewAdapter(
|
||||||
|
private val videos: MutableList<IPlatformVideo>,
|
||||||
|
private val inflater: LayoutInflater,
|
||||||
|
private val fragment: MainFragment,
|
||||||
|
private val overlayQualityContainer: FrameLayout,
|
||||||
|
private val isChannelShortsMode: () -> Boolean,
|
||||||
|
private val onNearEnd: () -> Unit,
|
||||||
|
) : RecyclerView.Adapter<CustomViewHolder>() {
|
||||||
|
val onResetTriggered = Event0()
|
||||||
|
var previousShownView: ShortView? = null
|
||||||
|
var newPosition: Int? = null
|
||||||
|
var needToPlay: Int? = null
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
|
||||||
|
val shortView = ShortView(inflater, fragment, overlayQualityContainer)
|
||||||
|
shortView.onResetTriggered.subscribe {
|
||||||
|
onResetTriggered.emit()
|
||||||
|
}
|
||||||
|
return CustomViewHolder(shortView)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
|
||||||
|
Logger.i(TAG, "Shorts change (position: ${position}): ${videos[position].name} (${videos[position].id.value})")
|
||||||
|
holder.shortView.changeVideo(videos[position], isChannelShortsMode())
|
||||||
|
|
||||||
|
if (position == itemCount - 1) {
|
||||||
|
onNearEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewRecycled(holder: CustomViewHolder) {
|
||||||
|
super.onViewRecycled(holder)
|
||||||
|
holder.shortView.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewAttachedToWindow(holder: CustomViewHolder) {
|
||||||
|
super.onViewAttachedToWindow(holder)
|
||||||
|
|
||||||
|
if (holder.absoluteAdapterPosition == needToPlay) {
|
||||||
|
holder.shortView.play()
|
||||||
|
needToPlay = null
|
||||||
|
previousShownView = holder.shortView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = videos.size
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
class CustomViewHolder(val shortView: ShortView) : RecyclerView.ViewHolder(shortView)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user