mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
226 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bce93b8e0f | |||
| 9a950958f9 | |||
| b6676e7763 | |||
| 35fe093e5c | |||
| 7cad4fbe07 | |||
| 240772790d | |||
| d659ecc518 | |||
| 7d8bb20b71 | |||
| 1cf5f776d5 | |||
| 137ba85538 | |||
| 642d218c54 | |||
| 26b5470200 | |||
| 547fe7bc13 | |||
| 678305e366 | |||
| 9f07673d85 | |||
| 19429263a9 | |||
| 986652adab | |||
| 4d93a58d5d | |||
| 817c90f3af | |||
| 77348b3787 | |||
| 31e26d03c6 | |||
| 1ef566ab16 | |||
| 7597f5136c | |||
| 9a2a70622f | |||
| 4fc33411fd | |||
| a9bb900994 | |||
| 8c1a18d8b4 | |||
| 14ae5f1572 | |||
| ed40994600 | |||
| 90e8c35b19 | |||
| 4d017ad357 | |||
| 2ca2a9db23 | |||
| 713d46c781 | |||
| 0429665173 | |||
| ac05edca77 | |||
| ad3dacf68f | |||
| 91a8996c11 | |||
| 40c4a51a2b | |||
| f8e0aaf4d2 | |||
| ad97b5a406 | |||
| b0e0c1b75f | |||
| b1fce443e9 | |||
| 66f8711055 | |||
| b7c123c281 | |||
| 9481bbf3f1 | |||
| 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 | |||
| 940bed2cee | |||
| 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 | |||
| 4eb20a1843 | |||
| 98c6378148 | |||
| bb066a7a31 | |||
| b5d3261f03 | |||
| 4702787784 | |||
| 30c41044da | |||
| e369676808 | |||
| 2fa9e65bee | |||
| cf96bd1ec0 | |||
| 1f5a069877 | |||
| adc5013ea4 | |||
| 515c5e00e9 | |||
| ba9f843368 | |||
| 0653f88c49 | |||
| 4ce9f64808 | |||
| 755bebaecb | |||
| 4fa0229ccb | |||
| 42dd8d6152 | |||
| 0a839b4814 | |||
| 586db317dd | |||
| ae36a24ad1 | |||
| 9a435f8859 | |||
| 81162c5df2 | |||
| c7c3ddfc96 | |||
| 830d3a9022 | |||
| a1c2d19daf | |||
| 004e4be4d3 | |||
| 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
|
||||||
|
|||||||
+12
-4
@@ -39,7 +39,7 @@ protobuf {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.futo.platformplayer'
|
namespace 'com.futo.platformplayer'
|
||||||
compileSdk 34
|
compileSdk 36
|
||||||
flavorDimensions "buildType"
|
flavorDimensions "buildType"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
stable {
|
||||||
@@ -97,7 +97,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 34
|
targetSdk 35
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|
||||||
@@ -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'
|
||||||
@@ -229,4 +231,10 @@ dependencies {
|
|||||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
|
||||||
|
//Rust casting SDK
|
||||||
|
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
|
||||||
|
// Polycentricandroid includes this
|
||||||
|
exclude group: 'net.java.dev.jna'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,30 +153,30 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.TestActivity"
|
android:name=".activities.TestActivity"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CaptchaActivity"
|
android:name=".activities.CaptchaActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceActivity"
|
android:name=".activities.AddSourceActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -189,54 +189,54 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity"
|
android:name=".activities.AddSourceOptionsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricHomeActivity"
|
android:name=".activities.PolycentricHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricBackupActivity"
|
android:name=".activities.PolycentricBackupActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricCreateProfileActivity"
|
android:name=".activities.PolycentricCreateProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricProfileActivity"
|
android:name=".activities.PolycentricProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
android:name=".activities.PolycentricWhyActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricImportProfileActivity"
|
android:name=".activities.PolycentricImportProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncHomeActivity"
|
android:name=".activities.SyncHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncPairActivity"
|
android:name=".activities.SyncPairActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncShowPairingCodeActivity"
|
android:name=".activities.SyncShowPairingCodeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1022,15 +1022,35 @@
|
|||||||
return x.value
|
return x.value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let settingsToUse = __DEV_SETTINGS ?? {};
|
||||||
|
if (true) {
|
||||||
|
for (let setting of this.Plugin?.currentPlugin?.settings) {
|
||||||
|
if (typeof settingsToUse[setting.variable] == "undefined") {
|
||||||
|
switch (setting?.type?.toLowerCase()) {
|
||||||
|
case "boolean":
|
||||||
|
settingsToUse[setting.variable] = setting.default === 'true';
|
||||||
|
break;
|
||||||
|
case "dropdown":
|
||||||
|
let dropDownIndex = parseInt(setting.default);
|
||||||
|
if (dropDownIndex) {
|
||||||
|
settingsToUse[setting.variable] = setting.options[dropDownIndex];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(name == "enable") {
|
if(name == "enable") {
|
||||||
if(parameterVals.length > 0)
|
if(parameterVals.length > 0)
|
||||||
parameterVals[0] = this.Plugin.currentPlugin;
|
parameterVals[0] = this.Plugin.currentPlugin;
|
||||||
else
|
else
|
||||||
parameterVals.push(this.Plugin.currentPlugin);
|
parameterVals.push(this.Plugin.currentPlugin);
|
||||||
if(parameterVals.length > 1)
|
if(parameterVals.length > 1)
|
||||||
parameterVals[1] = __DEV_SETTINGS;
|
parameterVals[1] = settingsToUse;
|
||||||
else
|
else
|
||||||
parameterVals.push(__DEV_SETTINGS);
|
parameterVals.push(settingsToUse);
|
||||||
}
|
}
|
||||||
|
|
||||||
const func = source[name];
|
const func = source[name];
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class ScriptException extends Error {
|
|||||||
super(arguments[0]);
|
super(arguments[0]);
|
||||||
this.plugin_type = "ScriptException";
|
this.plugin_type = "ScriptException";
|
||||||
this.message = arguments[0];
|
this.message = arguments[0];
|
||||||
|
this.msg = arguments[0];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
super(msg);
|
super(msg);
|
||||||
@@ -103,6 +104,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 +252,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 +468,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 +717,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 +795,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()
|
||||||
@@ -216,10 +216,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
|||||||
return InetAddress.getByAddress(this);
|
return InetAddress.getByAddress(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
|
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs: Int = 10_000): Socket? {
|
||||||
ensureNotMainThread()
|
ensureNotMainThread()
|
||||||
|
|
||||||
val timeout = 10000
|
|
||||||
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
||||||
if(addresses.isEmpty())
|
if(addresses.isEmpty())
|
||||||
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
|
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
|
||||||
@@ -232,7 +231,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
|
|||||||
val socket = Socket()
|
val socket = Socket()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeoutMs) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||||
socket.close()
|
socket.close()
|
||||||
@@ -263,7 +262,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.connect(InetSocketAddress(address, port), timeout);
|
socket.connect(InetSocketAddress(address, port), timeoutMs);
|
||||||
|
|
||||||
synchronized(syncObject) {
|
synchronized(syncObject) {
|
||||||
if (connectedSocket == null) {
|
if (connectedSocket == null) {
|
||||||
|
|||||||
@@ -2,10 +2,31 @@ 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.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.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.CancellationException
|
||||||
|
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.CountDownLatch
|
||||||
|
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
|
||||||
|
|
||||||
|
|
||||||
//V8
|
//V8
|
||||||
@@ -24,6 +45,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 +114,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 +193,198 @@ 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 {
|
||||||
|
if(p0 is V8ValueObject)
|
||||||
|
p0.setWeak();
|
||||||
|
promiseResult = p0 as T;
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
override fun onRejected(p0: V8Value?) {
|
||||||
|
promiseException = p0?.toException(plugin.config);
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
override fun onCatch(p0: V8Value?) {
|
||||||
|
promiseException = p0?.toException(plugin.config);
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.registerPromise(this) {
|
||||||
|
promiseException = CancellationException("Cancelled by system");
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
|
||||||
|
|
||||||
|
|
||||||
|
if(!promise.isPending) {
|
||||||
|
try {
|
||||||
|
Logger.i("V8", "V8Promise resolved synchronously");
|
||||||
|
if(promise.isFulfilled)
|
||||||
|
promiseResult = promise.getResult<T>();
|
||||||
|
else
|
||||||
|
promiseException = promise.getResult<V8Value>().toException(plugin.config);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
promiseException = ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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?) {
|
||||||
|
try {
|
||||||
|
plugin.resolvePromise(promise);
|
||||||
|
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented..");
|
||||||
|
Logger.i("V8", "Promise rejected, setting exception");
|
||||||
|
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("V8", "Rejection handling failed?" , ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onCatch(p0: V8Value?) {
|
||||||
|
try {
|
||||||
|
plugin.resolvePromise(promise);
|
||||||
|
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented..");
|
||||||
|
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("V8", "Catching handling failed?" , ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
plugin.registerPromise(promise) {
|
||||||
|
if(def.isActive)
|
||||||
|
def.cancel("Cancelled by system");
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun V8Value.toException(config: IV8PluginConfig): Throwable {
|
||||||
|
val p0 = this;
|
||||||
|
if(p0 is V8ValueObject) {
|
||||||
|
return V8Plugin.getExceptionFromPlugin(config, p0, null, null, null, "P:");
|
||||||
|
/*
|
||||||
|
val pluginType = p0.getOrDefault(config, "plugin_type", "Promise Exception", "")?.let { if(!it.isNullOrBlank()) it + "" else "" }
|
||||||
|
val msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
|
||||||
|
?: p0.getOrDefault(config, "message", "Promise Exception", "");
|
||||||
|
return Throwable("Promise Failed: " + pluginType + msg);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
else if(p0 is V8ValueString)
|
||||||
|
return Throwable("Promise Failed:" + p0.value);
|
||||||
|
else
|
||||||
|
return NotImplementedError("onCatch promise not implemented..");
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> Deferred<T>.awaitCancelConverted(): T {
|
||||||
|
try {
|
||||||
|
return this.await();
|
||||||
|
}
|
||||||
|
catch(ex: CancellationException) {
|
||||||
|
if(ex.cause != null) {
|
||||||
|
throw ex.cause!!;
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat.Type
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.core.view.doOnAttach
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class RootInsetsController private constructor(
|
||||||
|
private val activity: Activity,
|
||||||
|
private val window: Window,
|
||||||
|
private val root: ViewGroup
|
||||||
|
) {
|
||||||
|
private val controller by lazy { WindowInsetsControllerCompat(window, root) }
|
||||||
|
|
||||||
|
private val basePaddingLeft = root.paddingLeft
|
||||||
|
private val basePaddingTop = root.paddingTop
|
||||||
|
private val basePaddingRight = root.paddingRight
|
||||||
|
private val basePaddingBottom = root.paddingBottom
|
||||||
|
|
||||||
|
private var currentInsets: WindowInsetsCompat = WindowInsetsCompat.CONSUMED
|
||||||
|
private var fullscreen = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
window.statusBarColor = Color.TRANSPARENT
|
||||||
|
window.navigationBarColor = Color.TRANSPARENT
|
||||||
|
controller.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
|
||||||
|
currentInsets = insets
|
||||||
|
applyPadding()
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
root.doOnAttach { ViewCompat.requestApplyInsets(root) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun effectiveInsets(): Insets {
|
||||||
|
if (fullscreen) return Insets.NONE
|
||||||
|
|
||||||
|
val sys = currentInsets.getInsets(Type.systemBars())
|
||||||
|
val cut = currentInsets.getInsetsIgnoringVisibility(Type.displayCutout())
|
||||||
|
val portrait = activity.resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
val top = if (portrait) max(sys.top, cut.top) else sys.top
|
||||||
|
return Insets.of(sys.left, top, sys.right, sys.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun applyPadding() {
|
||||||
|
val e = effectiveInsets()
|
||||||
|
root.updatePadding(
|
||||||
|
left = basePaddingLeft + e.left,
|
||||||
|
top = basePaddingTop + e.top,
|
||||||
|
right = basePaddingRight + e.right,
|
||||||
|
bottom = basePaddingBottom + e.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forceRelayoutAndInsets() {
|
||||||
|
root.post {
|
||||||
|
ViewCompat.requestApplyInsets(root)
|
||||||
|
applyPadding()
|
||||||
|
root.post {
|
||||||
|
ViewCompat.requestApplyInsets(root)
|
||||||
|
applyPadding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enterFullscreen(allowCutoutShortEdges: Boolean = true) {
|
||||||
|
fullscreen = true
|
||||||
|
if (allowCutoutShortEdges) {
|
||||||
|
window.attributes = window.attributes.apply {
|
||||||
|
layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.hide(Type.systemBars())
|
||||||
|
forceRelayoutAndInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exitFullscreen() {
|
||||||
|
fullscreen = false
|
||||||
|
window.attributes = window.attributes.apply {
|
||||||
|
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|
||||||
|
}
|
||||||
|
controller.show(Type.systemBars())
|
||||||
|
forceRelayoutAndInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onConfigurationChanged() {
|
||||||
|
forceRelayoutAndInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLightSystemBarAppearance(lightStatus: Boolean, lightNav: Boolean) {
|
||||||
|
controller.isAppearanceLightStatusBars = lightStatus
|
||||||
|
controller.isAppearanceLightNavigationBars = lightNav
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun attach(activity: Activity, root: ViewGroup): RootInsetsController {
|
||||||
|
return RootInsetsController(activity, activity.window, root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import com.futo.platformplayer.states.StateCache
|
|||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePayment
|
import com.futo.platformplayer.states.StatePayment
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.states.StateSync
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
@@ -34,6 +35,7 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
|||||||
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.FormFieldButton
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -201,6 +203,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
8 -> "zh";
|
8 -> "zh";
|
||||||
9 -> "ru";
|
9 -> "ru";
|
||||||
10 -> "ar";
|
10 -> "ar";
|
||||||
|
11 -> "it";
|
||||||
|
12 -> "tr";
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,21 +591,32 @@ 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;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.shorts_fit_video, FieldForm.TOGGLE, R.string.shorts_fit_video_description, 29)
|
||||||
|
@FormFieldWarning(R.string.shorts_fit_video_warning)
|
||||||
|
var shortsFitVideo: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
@@ -704,6 +719,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var allowLinkLocalIpv4: Boolean = false;
|
var allowLinkLocalIpv4: Boolean = false;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
var experimentalCasting: Boolean = false
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
@@ -1017,8 +1037,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;
|
||||||
@@ -1086,6 +1106,39 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
||||||
var localConnections: Boolean = true;
|
var localConnections: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var syncServerUrl: String? = null;
|
||||||
|
@FormField(R.string.relay_server, FieldForm.READONLYTEXT, -1, 6)
|
||||||
|
val syncServer: String get() = if(syncServerUrl?.isBlank() == true) StateSync.RELAY_SERVER else syncServerUrl ?: StateSync.RELAY_SERVER;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
|
||||||
|
fun configureSyncServer() {
|
||||||
|
SettingsActivity.getActivity()?.let { context ->
|
||||||
|
UIDialogs.showDialog(context, R.drawable.device_sync, false,
|
||||||
|
"Enter the url to your relay server",
|
||||||
|
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
|
||||||
|
null,
|
||||||
|
syncServerUrl ?: "",
|
||||||
|
"YourRelayServerDomain.com", 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Reset", {
|
||||||
|
syncServerUrl = null;
|
||||||
|
instance.save();
|
||||||
|
context.reloadSettings();
|
||||||
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
|
}, UIDialogs.ActionStyle.ACCENT),
|
||||||
|
UIDialogs.Action.withInput("Configure", {
|
||||||
|
syncServerUrl = it?.text
|
||||||
|
instance.save();
|
||||||
|
context.reloadSettings();
|
||||||
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ class UIDialogs {
|
|||||||
currentDialog.code,
|
currentDialog.code,
|
||||||
currentDialog.defaultCloseAction,
|
currentDialog.defaultCloseAction,
|
||||||
*currentDialog.actions.map {
|
*currentDialog.actions.map {
|
||||||
return@map Action(it.text, {
|
return@map Action.withInput(it.text, { str ->
|
||||||
it.action();
|
it.invokeAction(str);
|
||||||
multiShowDialog(context, dialogDescriptor.drop(1), finally);
|
multiShowDialog(context, dialogDescriptor.drop(1), finally);
|
||||||
}, it.style);
|
}, it.style);
|
||||||
}.toTypedArray());
|
}.toTypedArray());
|
||||||
@@ -203,7 +203,9 @@ class UIDialogs {
|
|||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
||||||
}
|
}
|
||||||
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog
|
||||||
|
= showDialog(context, icon, animated, text, textDetails, code, null, null, defaultCloseAction, *actions);
|
||||||
|
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, input: String?, placeholder: String?, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
val builder = AlertDialog.Builder(context);
|
val builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
builder.setView(view);
|
builder.setView(view);
|
||||||
@@ -226,6 +228,16 @@ class UIDialogs {
|
|||||||
this.text = textDetails;
|
this.text = textDetails;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var inputView = view.findViewById<TextView>(R.id.dialog_text_input);
|
||||||
|
inputView.apply {
|
||||||
|
if (input == null && placeholder == null) this.visibility = View.GONE;
|
||||||
|
else {
|
||||||
|
this.text = input ?: "";
|
||||||
|
this.hint = placeholder ?: "";
|
||||||
|
this.visibility = View.VISIBLE;
|
||||||
|
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
|
||||||
|
}
|
||||||
|
};
|
||||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||||
if (code == null) this.visibility = View.GONE;
|
if (code == null) this.visibility = View.GONE;
|
||||||
else {
|
else {
|
||||||
@@ -250,7 +262,7 @@ class UIDialogs {
|
|||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
buttonView.typeface = resources.getFont(R.font.inter_regular);
|
buttonView.typeface = resources.getFont(R.font.inter_regular);
|
||||||
buttonView.text = act.text;
|
buttonView.text = act.text;
|
||||||
buttonView.setOnClickListener { act.action(); dialog.dismiss(); };
|
buttonView.setOnClickListener { act.invokeAction(DialogResult(inputView?.text?.toString())); dialog.dismiss(); };
|
||||||
when(act.style) {
|
when(act.style) {
|
||||||
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
|
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
|
||||||
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
|
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
|
||||||
@@ -275,7 +287,7 @@ class UIDialogs {
|
|||||||
};
|
};
|
||||||
dialog.setOnCancelListener {
|
dialog.setOnCancelListener {
|
||||||
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
|
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
|
||||||
actions[defaultCloseAction].action();
|
actions[defaultCloseAction].invokeAction(DialogResult(inputView?.text?.toString()));
|
||||||
}
|
}
|
||||||
dialog.setOnDismissListener {
|
dialog.setOnDismissListener {
|
||||||
registerDialogClosed(dialog);
|
registerDialogClosed(dialog);
|
||||||
@@ -424,7 +436,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 +444,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 +457,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();
|
||||||
}
|
}
|
||||||
@@ -531,17 +547,36 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
class Action {
|
class Action {
|
||||||
val text: String;
|
val text: String;
|
||||||
val action: ()->Unit;
|
val action: ((DialogResult?)->Unit);
|
||||||
val style: ActionStyle;
|
val style: ActionStyle;
|
||||||
var center: Boolean;
|
var center: Boolean;
|
||||||
|
|
||||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
|
this.text = text;
|
||||||
|
this.action = { action() };
|
||||||
|
this.style = style;
|
||||||
|
this.center = center;
|
||||||
|
}
|
||||||
|
protected constructor(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
this.center = center;
|
this.center = center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun invokeAction(input: DialogResult? = null) {
|
||||||
|
this.action(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun withInput(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false): Action {
|
||||||
|
return Action(text, action, style, center);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
class DialogResult(
|
||||||
|
val text: String?
|
||||||
|
);
|
||||||
enum class ActionStyle {
|
enum class ActionStyle {
|
||||||
NONE,
|
NONE,
|
||||||
PRIMARY,
|
PRIMARY,
|
||||||
|
|||||||
@@ -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,9 +75,26 @@ 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;
|
||||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||||
_textUrl.setText(url ?: "");
|
_textUrl.setText(url ?: "");
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(!isFirstLoad)
|
if(!isFirstLoad)
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
isFirstLoad = false;
|
isFirstLoad = false;
|
||||||
@@ -86,6 +104,35 @@ 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)", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import android.os.StrictMode.VmPolicy
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
@@ -32,14 +31,15 @@ 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
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.RootInsetsController
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
@@ -63,6 +63,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 +115,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 +171,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;
|
||||||
@@ -198,6 +199,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
private var _privateModeEnabled = false
|
private var _privateModeEnabled = false
|
||||||
private var _pictureInPictureEnabled = false
|
private var _pictureInPictureEnabled = false
|
||||||
private var _isFullscreen = false
|
private var _isFullscreen = false
|
||||||
|
private lateinit var _rootInsetsController: RootInsetsController
|
||||||
|
|
||||||
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
@@ -283,9 +285,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
|
||||||
window.attributes.layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
try {
|
try {
|
||||||
@@ -300,6 +299,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
FragmentedStorage.get<Settings>();
|
FragmentedStorage.get<Settings>();
|
||||||
|
|
||||||
rootView = findViewById(R.id.rootView);
|
rootView = findViewById(R.id.rootView);
|
||||||
|
_rootInsetsController = RootInsetsController.attach(this, rootView)
|
||||||
|
_rootInsetsController.setLightSystemBarAppearance(lightStatus = false, lightNav = false)
|
||||||
|
|
||||||
_fragContainerTopBar = findViewById(R.id.fragment_top_bar);
|
_fragContainerTopBar = findViewById(R.id.fragment_top_bar);
|
||||||
_fragContainerMain = findViewById(R.id.fragment_main);
|
_fragContainerMain = findViewById(R.id.fragment_main);
|
||||||
_fragContainerBotBar = findViewById(R.id.fragment_bottom_bar);
|
_fragContainerBotBar = findViewById(R.id.fragment_bottom_bar);
|
||||||
@@ -340,6 +342,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();
|
||||||
@@ -409,6 +412,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "onFullscreenChanged ${it}");
|
Logger.i(TAG, "onFullscreenChanged ${it}");
|
||||||
_isFullscreen = it
|
_isFullscreen = it
|
||||||
updatePrivateModeVisibility()
|
updatePrivateModeVisibility()
|
||||||
|
if (it) {
|
||||||
|
_rootInsetsController.enterFullscreen(allowCutoutShortEdges = Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||||
|
} else {
|
||||||
|
_rootInsetsController.exitFullscreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fragVideoDetail.onMinimize.subscribe {
|
_fragVideoDetail.onMinimize.subscribe {
|
||||||
@@ -610,6 +618,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//startActivity(Intent(this, TestActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -635,6 +645,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
private var _qrCodeLoadingDialog: AlertDialog? = null
|
private var _qrCodeLoadingDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
_rootInsetsController.onConfigurationChanged()
|
||||||
|
}
|
||||||
|
|
||||||
fun showUrlQrCodeScanner() {
|
fun showUrlQrCodeScanner() {
|
||||||
try {
|
try {
|
||||||
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
|
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
|
||||||
@@ -765,7 +780,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
handleUrlAll(targetData)
|
handleUrlAll(targetData, intent)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
||||||
}
|
}
|
||||||
@@ -776,8 +791,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleUrlAll(url: String) {
|
suspend fun handleUrlAll(url: String, openIntent: Intent? = null) {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
|
val intent = openIntent ?: this.intent;
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
if (url.startsWith("grayjay://license/")) {
|
if (url.startsWith("grayjay://license/")) {
|
||||||
@@ -804,11 +820,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"content" -> {
|
"content" -> {
|
||||||
if (!handleContent(url, intent.type)) {
|
if (!handleContent(url, intent?.type)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
getString(R.string.unknown_content_format) + " [${url}]\n[${intent?.type}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -929,6 +945,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
||||||
return handleUnknownText(String(data));
|
return handleUnknownText(String(data));
|
||||||
}
|
}
|
||||||
|
else if (mime?.let { it.startsWith("video/") || it.startsWith("audio/") } ?: false) {
|
||||||
|
val mediaItem = LocalVideoDetails.fromContent(file, mime);
|
||||||
|
navigateWhenReady(_fragVideoDetail, mediaItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1043,7 +1065,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "handleFCast");
|
Logger.i(TAG, "handleFCast");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StateCasting.instance.handleUrl(this, url)
|
StateCasting.instance.handleUrl(url)
|
||||||
return true;
|
return true;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||||
@@ -1253,6 +1275,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 {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
|
StateSync.instance.syncService?.connect(deviceInfo, true) { complete, message ->
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (complete != null) {
|
if (complete != null) {
|
||||||
if (complete) {
|
if (complete) {
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-3
@@ -2,10 +2,24 @@ package com.futo.platformplayer.api.media.models.streams
|
|||||||
|
|
||||||
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.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
|
|
||||||
class LocalVideoUnMuxedSourceDescriptor(private val video: VideoLocal) : VideoUnMuxedSourceDescriptor() {
|
class LocalVideoUnMuxedSourceDescriptor : VideoUnMuxedSourceDescriptor {
|
||||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
override val videoSources: Array<IVideoSource>;
|
||||||
override val audioSources: Array<IAudioSource> get() = video.audioSource.toTypedArray();
|
override val audioSources: Array<IAudioSource>;
|
||||||
|
|
||||||
|
constructor(video: VideoLocal) {
|
||||||
|
videoSources = video.videoSource.toTypedArray();
|
||||||
|
audioSources = video.audioSource.toTypedArray();
|
||||||
|
}
|
||||||
|
constructor(audio: LocalAudioContentSource) {
|
||||||
|
videoSources = arrayOf()
|
||||||
|
audioSources = arrayOf(audio);
|
||||||
|
}
|
||||||
|
constructor(videoSources: Array<IVideoSource>, audioSources: Array<IAudioSource>) {
|
||||||
|
this.videoSources = videoSources;
|
||||||
|
this.audioSources = audioSources;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -14,7 +14,8 @@ class AudioUrlSource(
|
|||||||
override val language: String = Language.UNKNOWN,
|
override val language: String = Language.UNKNOWN,
|
||||||
override val duration: Long? = null,
|
override val duration: Long? = null,
|
||||||
override var priority: Boolean = false,
|
override var priority: Boolean = false,
|
||||||
override var original: Boolean = false
|
override var original: Boolean = false,
|
||||||
|
var isLocal: Boolean = false
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -14,7 +14,8 @@ open class VideoUrlSource(
|
|||||||
override val codec : String = "",
|
override val codec : String = "",
|
||||||
override val bitrate : Int? = 0,
|
override val bitrate : Int? = 0,
|
||||||
|
|
||||||
override var priority: Boolean = false
|
override var priority: Boolean = false,
|
||||||
|
var isLocal: Boolean = false
|
||||||
) : IVideoUrlSource, IStreamMetaDataSource {
|
) : IVideoUrlSource, IStreamMetaDataSource {
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
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.playback.IPlaybackTracker
|
||||||
|
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.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||||
|
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.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.LocalVideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
open class LocalVideoDetails(
|
||||||
|
override val id: PlatformID,
|
||||||
|
override val name: String,
|
||||||
|
override val thumbnails: Thumbnails,
|
||||||
|
override val author: PlatformAuthorLink,
|
||||||
|
override val url: String,
|
||||||
|
override val duration: Long,
|
||||||
|
|
||||||
|
val mimeType: String? = null,
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override val datetime: OffsetDateTime?
|
||||||
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
|
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 dash: IDashManifestSource? get() = null;
|
||||||
|
override val hls: IHLSManifestSource? get() = null;
|
||||||
|
override val live: IVideoSource? get() = null;
|
||||||
|
|
||||||
|
|
||||||
|
override val shareUrl: String = ""
|
||||||
|
override val viewCount: Long = -1
|
||||||
|
override val rating: IRating = RatingLikes(0)
|
||||||
|
override val description: String = "";
|
||||||
|
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
|
||||||
|
(LocalVideoUnMuxedSourceDescriptor(
|
||||||
|
arrayOf(),
|
||||||
|
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name))
|
||||||
|
))
|
||||||
|
else (LocalVideoMuxedSourceDescriptor(
|
||||||
|
LocalVideoContentSource(url, mimeType ?: "", name)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
override val preview: ISerializedVideoSourceDescriptor? = null;
|
||||||
|
|
||||||
|
override val subtitles: List<SubtitleRawSource> = listOf()
|
||||||
|
override val isShort: Boolean = false
|
||||||
|
|
||||||
|
fun toJson() : String {
|
||||||
|
return Json.encodeToString(this);
|
||||||
|
}
|
||||||
|
fun fromJson(str : String) : SerializedPlatformVideoDetails {
|
||||||
|
return Serializer.json.decodeFromString<SerializedPlatformVideoDetails>(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromFile(name: String, filePath: String, mimeType: String? = null) : LocalVideoDetails {
|
||||||
|
if(filePath.startsWith("content://"))
|
||||||
|
return fromContent(filePath, mimeType);
|
||||||
|
|
||||||
|
return LocalVideoDetails(PlatformID("FILE", filePath, null, 0, -1),
|
||||||
|
name, Thumbnails(), PlatformAuthorLink.UNKNOWN, filePath, -1, mimeType, null);
|
||||||
|
}
|
||||||
|
fun fromContent(contentUrl: String, mimeType: String? = null) : LocalVideoDetails {
|
||||||
|
var nameToUse = getFileNameFromContentUrl(contentUrl) ?: "File";
|
||||||
|
|
||||||
|
return LocalVideoDetails(PlatformID("FILE", contentUrl, null, 0, -1),
|
||||||
|
nameToUse, Thumbnails(), PlatformAuthorLink.UNKNOWN, contentUrl, -1, mimeType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Range")
|
||||||
|
private fun getFileNameFromContentUrl(url: String): String? {
|
||||||
|
val cursor = StateApp.instance.context.contentResolver.query(url.toUri(), null, null, null, null);
|
||||||
|
cursor?.moveToFirst();
|
||||||
|
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||||
|
cursor?.close();
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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;
|
||||||
|
|||||||
+14
-4
@@ -1,13 +1,23 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.local.models
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||||
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.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
class LocalVideoMuxedSourceDescriptor(
|
class LocalVideoMuxedSourceDescriptor: VideoMuxedSourceDescriptor {
|
||||||
private val video: LocalVideoFileSource
|
override val videoSources: Array<IVideoSource>;
|
||||||
) : VideoMuxedSourceDescriptor() {
|
|
||||||
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
|
constructor(video: LocalVideoFileSource) {
|
||||||
|
videoSources = arrayOf(video);
|
||||||
|
}
|
||||||
|
constructor(video: LocalVideoContentSource) {
|
||||||
|
videoSources = arrayOf(video);
|
||||||
|
}
|
||||||
|
constructor(videoSources: Array<IVideoSource>) {
|
||||||
|
this.videoSources = videoSources;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalAudioContentSource : IAudioSource {
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
override val language: String = Language.UNKNOWN
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
|
var contentUrl: String;
|
||||||
|
|
||||||
|
constructor(contentUrl: String, mime: String, name: String? = null) {
|
||||||
|
this.name = name ?: "File";
|
||||||
|
container = mime;
|
||||||
|
duration = 0;
|
||||||
|
|
||||||
|
this.contentUrl = contentUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalAudioFileSource: IAudioSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
override val language: String = Language.UNKNOWN;
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
|
var file: File;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
this.file = file;
|
||||||
|
name = file.name;
|
||||||
|
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalVideoContentSource: IVideoSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val width: Int;
|
||||||
|
override val height: Int;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
var contentUrl: String;
|
||||||
|
|
||||||
|
constructor(contentUrl: String, mime: String, name: String? = null) {
|
||||||
|
this.name = name ?: "File";
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
container = mime;
|
||||||
|
duration = 0;
|
||||||
|
this.contentUrl = contentUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -20,7 +20,10 @@ class LocalVideoFileSource: IVideoSource {
|
|||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean = false;
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
var file: File;
|
||||||
|
|
||||||
constructor(file: File) {
|
constructor(file: File) {
|
||||||
|
this.file = file;
|
||||||
name = file.name;
|
name = file.name;
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 0;
|
height = 0;
|
||||||
|
|||||||
+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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class AirPlayCastingDevice : CastingDevice {
|
class AirPlayCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: https://nto.github.io/AirPlay
|
//See for more info: https://nto.github.io/AirPlay
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY;
|
override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY;
|
||||||
|
|||||||
@@ -2,147 +2,78 @@ package com.futo.platformplayer.casting
|
|||||||
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import kotlinx.serialization.KSerializer
|
import org.fcast.sender_sdk.Metadata
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
|
||||||
enum class CastConnectionState {
|
|
||||||
DISCONNECTED,
|
|
||||||
CONNECTING,
|
|
||||||
CONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
|
|
||||||
enum class CastProtocolType {
|
|
||||||
CHROMECAST,
|
|
||||||
AIRPLAY,
|
|
||||||
FCAST;
|
|
||||||
|
|
||||||
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: CastProtocolType) {
|
|
||||||
encoder.encodeString(value.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): CastProtocolType {
|
|
||||||
val name = decoder.decodeString()
|
|
||||||
return when (name) {
|
|
||||||
"FASTCAST" -> FCAST // Handle the renamed case
|
|
||||||
else -> CastProtocolType.valueOf(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class CastingDevice {
|
abstract class CastingDevice {
|
||||||
abstract val protocol: CastProtocolType;
|
abstract val isReady: Boolean
|
||||||
abstract val isReady: Boolean;
|
abstract val usedRemoteAddress: InetAddress?
|
||||||
abstract var usedRemoteAddress: InetAddress?;
|
abstract val localAddress: InetAddress?
|
||||||
abstract var localAddress: InetAddress?;
|
abstract val name: String?
|
||||||
abstract val canSetVolume: Boolean;
|
abstract val onConnectionStateChanged: Event1<CastConnectionState>
|
||||||
abstract val canSetSpeed: Boolean;
|
abstract val onPlayChanged: Event1<Boolean>
|
||||||
|
abstract val onTimeChanged: Event1<Double>
|
||||||
|
abstract val onDurationChanged: Event1<Double>
|
||||||
|
abstract val onVolumeChanged: Event1<Double>
|
||||||
|
abstract val onSpeedChanged: Event1<Double>
|
||||||
|
abstract var connectionState: CastConnectionState
|
||||||
|
abstract val protocolType: CastProtocolType
|
||||||
|
abstract var isPlaying: Boolean
|
||||||
|
abstract val expectedCurrentTime: Double
|
||||||
|
abstract var speed: Double
|
||||||
|
abstract var time: Double
|
||||||
|
abstract var duration: Double
|
||||||
|
abstract var volume: Double
|
||||||
|
abstract fun canSetVolume(): Boolean
|
||||||
|
abstract fun canSetSpeed(): Boolean
|
||||||
|
|
||||||
var name: String? = null;
|
@Throws
|
||||||
var isPlaying: Boolean = false
|
abstract fun resumePlayback()
|
||||||
set(value) {
|
|
||||||
val changed = value != field;
|
|
||||||
field = value;
|
|
||||||
if (changed) {
|
|
||||||
onPlayChanged.emit(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private var lastTimeChangeTime_ms: Long = 0
|
@Throws
|
||||||
var time: Double = 0.0
|
abstract fun pausePlayback()
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
|
abstract fun stopPlayback()
|
||||||
time = value
|
|
||||||
lastTimeChangeTime_ms = changeTime_ms
|
|
||||||
onTimeChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastDurationChangeTime_ms: Long = 0
|
@Throws
|
||||||
var duration: Double = 0.0
|
abstract fun seekTo(timeSeconds: Double)
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
|
abstract fun changeVolume(timeSeconds: Double)
|
||||||
duration = value
|
|
||||||
lastDurationChangeTime_ms = changeTime_ms
|
|
||||||
onDurationChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastVolumeChangeTime_ms: Long = 0
|
@Throws
|
||||||
var volume: Double = 1.0
|
abstract fun changeSpeed(speed: Double)
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
|
abstract fun connect()
|
||||||
volume = value
|
|
||||||
lastVolumeChangeTime_ms = changeTime_ms
|
|
||||||
onVolumeChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastSpeedChangeTime_ms: Long = 0
|
@Throws
|
||||||
var speed: Double = 1.0
|
abstract fun disconnect()
|
||||||
private set
|
abstract fun getDeviceInfo(): CastingDeviceInfo
|
||||||
|
abstract fun getAddresses(): List<InetAddress>
|
||||||
|
|
||||||
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
|
abstract fun loadVideo(
|
||||||
speed = value
|
streamType: String,
|
||||||
lastSpeedChangeTime_ms = changeTime_ms
|
contentType: String,
|
||||||
onSpeedChanged.emit(value)
|
contentId: String,
|
||||||
}
|
resumePosition: Double,
|
||||||
}
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
)
|
||||||
|
|
||||||
val expectedCurrentTime: Double
|
@Throws
|
||||||
get() {
|
abstract fun loadContent(
|
||||||
val diff = if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
contentType: String,
|
||||||
return time + diff;
|
content: String,
|
||||||
};
|
resumePosition: Double,
|
||||||
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
|
duration: Double,
|
||||||
set(value) {
|
speed: Double?,
|
||||||
val changed = value != field;
|
metadata: Metadata?
|
||||||
field = value;
|
)
|
||||||
|
|
||||||
if (changed) {
|
abstract fun ensureThreadStarted()
|
||||||
onConnectionStateChanged.emit(value);
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var onConnectionStateChanged = Event1<CastConnectionState>();
|
|
||||||
var onPlayChanged = Event1<Boolean>();
|
|
||||||
var onTimeChanged = Event1<Double>();
|
|
||||||
var onDurationChanged = Event1<Double>();
|
|
||||||
var onVolumeChanged = Event1<Double>();
|
|
||||||
var onSpeedChanged = Event1<Double>();
|
|
||||||
|
|
||||||
abstract fun stopCasting();
|
|
||||||
|
|
||||||
abstract fun seekVideo(timeSeconds: Double);
|
|
||||||
abstract fun stopVideo();
|
|
||||||
abstract fun pauseVideo();
|
|
||||||
abstract fun resumeVideo();
|
|
||||||
abstract fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?);
|
|
||||||
abstract fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?);
|
|
||||||
open fun changeVolume(volume: Double) { throw NotImplementedError() }
|
|
||||||
open fun changeSpeed(speed: Double) { throw NotImplementedError() }
|
|
||||||
|
|
||||||
abstract fun start();
|
|
||||||
abstract fun stop();
|
|
||||||
|
|
||||||
abstract fun getDeviceInfo(): CastingDeviceInfo;
|
|
||||||
|
|
||||||
abstract fun getAddresses(): List<InetAddress>;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import org.fcast.sender_sdk.ApplicationInfo
|
||||||
|
import org.fcast.sender_sdk.GenericKeyEvent
|
||||||
|
import org.fcast.sender_sdk.GenericMediaEvent
|
||||||
|
import org.fcast.sender_sdk.PlaybackState
|
||||||
|
import org.fcast.sender_sdk.Source
|
||||||
|
import java.net.InetAddress
|
||||||
|
import org.fcast.sender_sdk.CastingDevice as RsCastingDevice;
|
||||||
|
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
|
||||||
|
import org.fcast.sender_sdk.DeviceConnectionState
|
||||||
|
import org.fcast.sender_sdk.DeviceFeature
|
||||||
|
import org.fcast.sender_sdk.IpAddr
|
||||||
|
import org.fcast.sender_sdk.LoadRequest
|
||||||
|
import org.fcast.sender_sdk.Metadata
|
||||||
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
|
import org.fcast.sender_sdk.urlFormatIpAddr
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
|
||||||
|
private fun ipAddrToInetAddress(addr: IpAddr): InetAddress = when (addr) {
|
||||||
|
is IpAddr.V4 -> Inet4Address.getByAddress(
|
||||||
|
byteArrayOf(
|
||||||
|
addr.o1.toByte(),
|
||||||
|
addr.o2.toByte(),
|
||||||
|
addr.o3.toByte(),
|
||||||
|
addr.o4.toByte()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
is IpAddr.V6 -> Inet6Address.getByAddress(
|
||||||
|
byteArrayOf(
|
||||||
|
addr.o1.toByte(),
|
||||||
|
addr.o2.toByte(),
|
||||||
|
addr.o3.toByte(),
|
||||||
|
addr.o4.toByte(),
|
||||||
|
addr.o5.toByte(),
|
||||||
|
addr.o6.toByte(),
|
||||||
|
addr.o7.toByte(),
|
||||||
|
addr.o8.toByte(),
|
||||||
|
addr.o9.toByte(),
|
||||||
|
addr.o10.toByte(),
|
||||||
|
addr.o11.toByte(),
|
||||||
|
addr.o12.toByte(),
|
||||||
|
addr.o13.toByte(),
|
||||||
|
addr.o14.toByte(),
|
||||||
|
addr.o15.toByte(),
|
||||||
|
addr.o16.toByte()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
||||||
|
class EventHandler : RsDeviceEventHandler {
|
||||||
|
var onConnectionStateChanged = Event1<DeviceConnectionState>();
|
||||||
|
var onPlayChanged = Event1<Boolean>()
|
||||||
|
var onTimeChanged = Event1<Double>()
|
||||||
|
var onDurationChanged = Event1<Double>()
|
||||||
|
var onVolumeChanged = Event1<Double>()
|
||||||
|
var onSpeedChanged = Event1<Double>()
|
||||||
|
|
||||||
|
override fun connectionStateChanged(state: DeviceConnectionState) {
|
||||||
|
onConnectionStateChanged.emit(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun volumeChanged(volume: Double) {
|
||||||
|
onVolumeChanged.emit(volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun timeChanged(time: Double) {
|
||||||
|
onTimeChanged.emit(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playbackStateChanged(state: PlaybackState) {
|
||||||
|
onPlayChanged.emit(state == PlaybackState.PLAYING)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun durationChanged(duration: Double) {
|
||||||
|
onDurationChanged.emit(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun speedChanged(speed: Double) {
|
||||||
|
onSpeedChanged.emit(speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sourceChanged(source: Source) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun keyEvent(event: GenericKeyEvent) {
|
||||||
|
// Unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mediaEvent(event: GenericMediaEvent) {
|
||||||
|
// Unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playbackError(message: String) {
|
||||||
|
Logger.e(TAG, "Playback error: $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventHandler = EventHandler()
|
||||||
|
override val isReady: Boolean
|
||||||
|
get() = device.isReady()
|
||||||
|
override val name: String
|
||||||
|
get() = device.name()
|
||||||
|
override var usedRemoteAddress: InetAddress? = null
|
||||||
|
override var localAddress: InetAddress? = null
|
||||||
|
override fun canSetVolume(): Boolean = device.supportsFeature(DeviceFeature.SET_VOLUME)
|
||||||
|
override fun canSetSpeed(): Boolean = device.supportsFeature(DeviceFeature.SET_SPEED)
|
||||||
|
|
||||||
|
override val onConnectionStateChanged =
|
||||||
|
Event1<CastConnectionState>()
|
||||||
|
override val onPlayChanged: Event1<Boolean>
|
||||||
|
get() = eventHandler.onPlayChanged
|
||||||
|
override val onTimeChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onTimeChanged
|
||||||
|
override val onDurationChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onDurationChanged
|
||||||
|
override val onVolumeChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onVolumeChanged
|
||||||
|
override val onSpeedChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onSpeedChanged
|
||||||
|
|
||||||
|
override fun resumePlayback() = device.resumePlayback()
|
||||||
|
override fun pausePlayback() = device.pausePlayback()
|
||||||
|
override fun stopPlayback() = device.stopPlayback()
|
||||||
|
override fun seekTo(timeSeconds: Double) = device.seek(timeSeconds)
|
||||||
|
override fun changeVolume(newVolume: Double) {
|
||||||
|
device.changeVolume(newVolume)
|
||||||
|
volume = newVolume
|
||||||
|
}
|
||||||
|
override fun changeSpeed(speed: Double) = device.changeSpeed(speed)
|
||||||
|
override fun connect() = device.connect(
|
||||||
|
ApplicationInfo(
|
||||||
|
"Grayjay Android",
|
||||||
|
"${BuildConfig.VERSION_NAME}-${BuildConfig.FLAVOR}",
|
||||||
|
"${Build.MANUFACTURER} ${Build.MODEL}"
|
||||||
|
),
|
||||||
|
eventHandler,
|
||||||
|
1000.toULong()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun disconnect() = device.disconnect()
|
||||||
|
|
||||||
|
override fun getDeviceInfo(): CastingDeviceInfo {
|
||||||
|
val info = device.getDeviceInfo()
|
||||||
|
return CastingDeviceInfo(
|
||||||
|
info.name,
|
||||||
|
when (info.protocol) {
|
||||||
|
ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST
|
||||||
|
ProtocolType.F_CAST -> CastProtocolType.FCAST
|
||||||
|
},
|
||||||
|
addresses = info.addresses.map { urlFormatIpAddr(it) }.toTypedArray(),
|
||||||
|
port = info.port.toInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAddresses(): List<InetAddress> = device.getAddresses().map {
|
||||||
|
ipAddrToInetAddress(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = device.load(
|
||||||
|
LoadRequest.Video(
|
||||||
|
contentType = contentType,
|
||||||
|
url = contentId,
|
||||||
|
resumePosition = resumePosition,
|
||||||
|
speed = speed,
|
||||||
|
volume = volume,
|
||||||
|
metadata = metadata
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = device.load(
|
||||||
|
LoadRequest.Content(
|
||||||
|
contentType = contentType,
|
||||||
|
content = content,
|
||||||
|
resumePosition = resumePosition,
|
||||||
|
speed = speed,
|
||||||
|
volume = volume,
|
||||||
|
metadata = metadata,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override var connectionState = CastConnectionState.DISCONNECTED
|
||||||
|
override val protocolType: CastProtocolType
|
||||||
|
get() = when (device.castingProtocol()) {
|
||||||
|
ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST
|
||||||
|
ProtocolType.F_CAST -> CastProtocolType.FCAST
|
||||||
|
}
|
||||||
|
override var volume: Double = 1.0
|
||||||
|
override var duration: Double = 0.0
|
||||||
|
private var lastTimeChangeTime_ms: Long = 0
|
||||||
|
override var time: Double = 0.0
|
||||||
|
override var speed: Double = 0.0
|
||||||
|
override var isPlaying: Boolean = false
|
||||||
|
|
||||||
|
override val expectedCurrentTime: Double
|
||||||
|
get() {
|
||||||
|
val diff =
|
||||||
|
if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
||||||
|
return time + diff
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
eventHandler.onConnectionStateChanged.subscribe { newState ->
|
||||||
|
when (newState) {
|
||||||
|
is DeviceConnectionState.Connected -> {
|
||||||
|
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
|
||||||
|
localAddress = ipAddrToInetAddress(newState.localAddr)
|
||||||
|
connectionState = CastConnectionState.CONNECTED
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.CONNECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceConnectionState.Connecting, DeviceConnectionState.Reconnecting -> {
|
||||||
|
connectionState = CastConnectionState.CONNECTING
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.CONNECTING)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceConnectionState.Disconnected -> {
|
||||||
|
connectionState = CastConnectionState.CONNECTING
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState == DeviceConnectionState.Disconnected) {
|
||||||
|
try {
|
||||||
|
Logger.i(TAG, "Stopping device")
|
||||||
|
device.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to stop device: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventHandler.onPlayChanged.subscribe { isPlaying = it }
|
||||||
|
eventHandler.onTimeChanged.subscribe {
|
||||||
|
lastTimeChangeTime_ms = System.currentTimeMillis()
|
||||||
|
time = it
|
||||||
|
}
|
||||||
|
eventHandler.onDurationChanged.subscribe { duration = it }
|
||||||
|
eventHandler.onVolumeChanged.subscribe { volume = it }
|
||||||
|
eventHandler.onSpeedChanged.subscribe { speed = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ensureThreadStarted() {}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "CastingDeviceExp"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import org.fcast.sender_sdk.Metadata
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
enum class CastConnectionState {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
|
||||||
|
enum class CastProtocolType {
|
||||||
|
CHROMECAST,
|
||||||
|
AIRPLAY,
|
||||||
|
FCAST;
|
||||||
|
|
||||||
|
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: CastProtocolType) {
|
||||||
|
encoder.encodeString(value.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): CastProtocolType {
|
||||||
|
val name = decoder.decodeString()
|
||||||
|
return when (name) {
|
||||||
|
"FASTCAST" -> FCAST // Handle the renamed case
|
||||||
|
else -> CastProtocolType.valueOf(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CastingDeviceLegacy {
|
||||||
|
abstract val protocol: CastProtocolType;
|
||||||
|
abstract val isReady: Boolean;
|
||||||
|
abstract var usedRemoteAddress: InetAddress?;
|
||||||
|
abstract var localAddress: InetAddress?;
|
||||||
|
abstract val canSetVolume: Boolean;
|
||||||
|
abstract val canSetSpeed: Boolean;
|
||||||
|
|
||||||
|
var name: String? = null;
|
||||||
|
var isPlaying: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
val changed = value != field;
|
||||||
|
field = value;
|
||||||
|
if (changed) {
|
||||||
|
onPlayChanged.emit(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private var lastTimeChangeTime_ms: Long = 0
|
||||||
|
var time: Double = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
|
||||||
|
time = value
|
||||||
|
lastTimeChangeTime_ms = changeTime_ms
|
||||||
|
onTimeChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastDurationChangeTime_ms: Long = 0
|
||||||
|
var duration: Double = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
|
||||||
|
duration = value
|
||||||
|
lastDurationChangeTime_ms = changeTime_ms
|
||||||
|
onDurationChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastVolumeChangeTime_ms: Long = 0
|
||||||
|
var volume: Double = 1.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
|
||||||
|
volume = value
|
||||||
|
lastVolumeChangeTime_ms = changeTime_ms
|
||||||
|
onVolumeChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastSpeedChangeTime_ms: Long = 0
|
||||||
|
var speed: Double = 1.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
|
||||||
|
speed = value
|
||||||
|
lastSpeedChangeTime_ms = changeTime_ms
|
||||||
|
onSpeedChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedCurrentTime: Double
|
||||||
|
get() {
|
||||||
|
val diff =
|
||||||
|
if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
||||||
|
return time + diff;
|
||||||
|
};
|
||||||
|
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
|
||||||
|
set(value) {
|
||||||
|
val changed = value != field;
|
||||||
|
field = value;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
onConnectionStateChanged.emit(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var onConnectionStateChanged = Event1<CastConnectionState>();
|
||||||
|
var onPlayChanged = Event1<Boolean>();
|
||||||
|
var onTimeChanged = Event1<Double>();
|
||||||
|
var onDurationChanged = Event1<Double>();
|
||||||
|
var onVolumeChanged = Event1<Double>();
|
||||||
|
var onSpeedChanged = Event1<Double>();
|
||||||
|
|
||||||
|
abstract fun stopCasting();
|
||||||
|
|
||||||
|
abstract fun seekVideo(timeSeconds: Double);
|
||||||
|
abstract fun stopVideo();
|
||||||
|
abstract fun pauseVideo();
|
||||||
|
abstract fun resumeVideo();
|
||||||
|
abstract fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?
|
||||||
|
);
|
||||||
|
|
||||||
|
abstract fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?
|
||||||
|
);
|
||||||
|
|
||||||
|
open fun changeVolume(volume: Double) {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun changeSpeed(speed: Double) {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun start();
|
||||||
|
abstract fun stop();
|
||||||
|
|
||||||
|
abstract fun getDeviceInfo(): CastingDeviceInfo;
|
||||||
|
|
||||||
|
abstract fun getAddresses(): List<InetAddress>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CastingDeviceLegacyWrapper(val inner: CastingDeviceLegacy) : CastingDevice() {
|
||||||
|
override val isReady: Boolean get() = inner.isReady
|
||||||
|
override val usedRemoteAddress: InetAddress? get() = inner.usedRemoteAddress
|
||||||
|
override val localAddress: InetAddress? get() = inner.localAddress
|
||||||
|
override val name: String? get() = inner.name
|
||||||
|
override val onConnectionStateChanged: Event1<CastConnectionState> get() = inner.onConnectionStateChanged
|
||||||
|
override val onPlayChanged: Event1<Boolean> get() = inner.onPlayChanged
|
||||||
|
override val onTimeChanged: Event1<Double> get() = inner.onTimeChanged
|
||||||
|
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
|
||||||
|
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
|
||||||
|
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
|
||||||
|
override var connectionState: CastConnectionState
|
||||||
|
get() = inner.connectionState
|
||||||
|
set(_) = Unit
|
||||||
|
override val protocolType: CastProtocolType get() = inner.protocol
|
||||||
|
override var isPlaying: Boolean
|
||||||
|
get() = inner.isPlaying
|
||||||
|
set(_) = Unit
|
||||||
|
override val expectedCurrentTime: Double
|
||||||
|
get() = inner.expectedCurrentTime
|
||||||
|
override var speed: Double
|
||||||
|
get() = inner.speed
|
||||||
|
set(_) = Unit
|
||||||
|
override var time: Double
|
||||||
|
get() = inner.time
|
||||||
|
set(_) = Unit
|
||||||
|
override var duration: Double
|
||||||
|
get() = inner.duration
|
||||||
|
set(_) = Unit
|
||||||
|
override var volume: Double
|
||||||
|
get() = inner.volume
|
||||||
|
set(_) = Unit
|
||||||
|
|
||||||
|
override fun canSetVolume(): Boolean = inner.canSetVolume
|
||||||
|
override fun canSetSpeed(): Boolean = inner.canSetSpeed
|
||||||
|
override fun resumePlayback() = inner.resumeVideo()
|
||||||
|
override fun pausePlayback() = inner.pauseVideo()
|
||||||
|
override fun stopPlayback() = inner.stopVideo()
|
||||||
|
override fun seekTo(timeSeconds: Double) = inner.seekVideo(timeSeconds)
|
||||||
|
override fun changeVolume(timeSeconds: Double) = inner.changeVolume(timeSeconds)
|
||||||
|
override fun changeSpeed(speed: Double) = inner.changeSpeed(speed)
|
||||||
|
override fun connect() = inner.start()
|
||||||
|
override fun disconnect() = inner.stop()
|
||||||
|
override fun getDeviceInfo(): CastingDeviceInfo = inner.getDeviceInfo()
|
||||||
|
override fun getAddresses(): List<InetAddress> = inner.getAddresses()
|
||||||
|
override fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = inner.loadVideo(streamType, contentType, contentId, resumePosition, duration, speed)
|
||||||
|
|
||||||
|
override fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = inner.loadContent(contentType, content, resumePosition, duration, speed)
|
||||||
|
|
||||||
|
override fun ensureThreadStarted() = when (inner) {
|
||||||
|
is FCastCastingDevice -> inner.ensureThreadStarted()
|
||||||
|
is ChromecastCastingDevice -> inner.ensureThreadsStarted()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ import javax.net.ssl.SSLSocket
|
|||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
class ChromecastCastingDevice : CastingDevice {
|
class ChromecastCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: https://developers.google.com/cast/docs/media/messages
|
//See for more info: https://developers.google.com/cast/docs/media/messages
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST;
|
override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST;
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.futo.platformplayer.casting
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
@@ -25,7 +24,6 @@ import com.futo.platformplayer.toInetAddress
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -34,7 +32,6 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
@@ -72,7 +69,7 @@ enum class Opcode(val value: Byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FCastCastingDevice : CastingDevice {
|
class FCastCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: TODO
|
//See for more info: TODO
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
|
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
|
||||||
@@ -348,7 +345,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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import org.fcast.sender_sdk.DeviceInfo as RsDeviceInfo
|
||||||
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
|
import org.fcast.sender_sdk.CastContext
|
||||||
|
import org.fcast.sender_sdk.NsdDeviceDiscoverer
|
||||||
|
|
||||||
|
class StateCastingExp : StateCasting() {
|
||||||
|
private val _context = CastContext()
|
||||||
|
var _deviceDiscoverer: NsdDeviceDiscoverer? = null
|
||||||
|
|
||||||
|
class DiscoveryEventHandler(
|
||||||
|
private val onDeviceAdded: (RsDeviceInfo) -> Unit,
|
||||||
|
private val onDeviceRemoved: (String) -> Unit,
|
||||||
|
private val onDeviceUpdated: (RsDeviceInfo) -> Unit,
|
||||||
|
) : org.fcast.sender_sdk.DeviceDiscovererEventHandler {
|
||||||
|
override fun deviceAvailable(deviceInfo: RsDeviceInfo) {
|
||||||
|
onDeviceAdded(deviceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceChanged(deviceInfo: RsDeviceInfo) {
|
||||||
|
onDeviceUpdated(deviceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceRemoved(deviceName: String) {
|
||||||
|
onDeviceRemoved(deviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
org.fcast.sender_sdk.initLogger(org.fcast.sender_sdk.LogLevelFilter.DEBUG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleUrl(url: String) {
|
||||||
|
try {
|
||||||
|
val foundDeviceInfo = org.fcast.sender_sdk.deviceInfoFromUrl(url)!!
|
||||||
|
val foundDevice = _context.createDeviceFromInfo(foundDeviceInfo)
|
||||||
|
connectDevice(CastingDeviceExp(foundDevice))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to handle URL: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
val ad = activeDevice ?: return
|
||||||
|
_resumeCastingDevice = ad.getDeviceInfo()
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
|
||||||
|
Logger.i(TAG, "Stopping active device because of onStop.")
|
||||||
|
try {
|
||||||
|
ad.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to disconnect from device: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun start(context: Context) {
|
||||||
|
if (_started)
|
||||||
|
return
|
||||||
|
_started = true
|
||||||
|
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set null start")
|
||||||
|
_resumeCastingDevice = null
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService starting...")
|
||||||
|
|
||||||
|
_castServer.start()
|
||||||
|
enableDeveloper(true)
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService started.")
|
||||||
|
|
||||||
|
_deviceDiscoverer = NsdDeviceDiscoverer(
|
||||||
|
context,
|
||||||
|
DiscoveryEventHandler(
|
||||||
|
{ deviceInfo -> // Added
|
||||||
|
Logger.i(TAG, "Device added: ${deviceInfo.name}")
|
||||||
|
val device = _context.createDeviceFromInfo(deviceInfo)
|
||||||
|
val deviceHandle = CastingDeviceExp(device)
|
||||||
|
devices[deviceHandle.device.name()] = deviceHandle
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
onDeviceAdded.emit(deviceHandle)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deviceName -> // Removed
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
if (devices.containsKey(deviceName)) {
|
||||||
|
val device = devices.remove(deviceName)
|
||||||
|
if (device != null) {
|
||||||
|
onDeviceRemoved.emit(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deviceInfo -> // Updated
|
||||||
|
Logger.i(TAG, "Device updated: $deviceInfo")
|
||||||
|
val handle = devices[deviceInfo.name]
|
||||||
|
if (handle != null && handle is CastingDeviceExp) {
|
||||||
|
handle.device.setPort(deviceInfo.port)
|
||||||
|
handle.device.setAddresses(deviceInfo.addresses)
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
onDeviceChanged.emit(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun stop() {
|
||||||
|
if (!_started) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_started = false
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
|
_scopeIO.cancel()
|
||||||
|
_scopeMain.cancel()
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopping active device because StateCasting is being stopped.")
|
||||||
|
val d = activeDevice
|
||||||
|
activeDevice = null
|
||||||
|
try {
|
||||||
|
d?.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to disconnect device: $e")
|
||||||
|
}
|
||||||
|
|
||||||
|
_castServer.stop()
|
||||||
|
_castServer.removeAllHandlers()
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopped.")
|
||||||
|
|
||||||
|
_deviceDiscoverer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startUpdateTimeJob(
|
||||||
|
onTimeJobTimeChanged_s: Event1<Long>,
|
||||||
|
setTime: (Long) -> Unit
|
||||||
|
): Job? = null
|
||||||
|
|
||||||
|
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp {
|
||||||
|
val rsAddrs =
|
||||||
|
deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } // Throws!
|
||||||
|
val rsDeviceInfo = RsDeviceInfo(
|
||||||
|
name = deviceInfo.name,
|
||||||
|
protocol = when (deviceInfo.type) {
|
||||||
|
com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST
|
||||||
|
com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
},
|
||||||
|
addresses = rsAddrs,
|
||||||
|
port = deviceInfo.port.toUShort(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "StateCastingExp"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.net.nsd.NsdManager
|
||||||
|
import android.net.nsd.NsdServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.net.InetAddress
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class StateCastingLegacy : StateCasting() {
|
||||||
|
private var _nsdManager: NsdManager? = null
|
||||||
|
|
||||||
|
private val _discoveryListeners = mapOf(
|
||||||
|
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
|
||||||
|
"_airplay._tcp" to createDiscoveryListener(::addOrUpdateAirPlayDevice),
|
||||||
|
"_fastcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice),
|
||||||
|
"_fcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun handleUrl(url: String) {
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
if (uri.scheme != "fcast") {
|
||||||
|
throw Exception("Expected scheme to be FCast")
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = uri.host
|
||||||
|
if (type != "r") {
|
||||||
|
throw Exception("Expected type r")
|
||||||
|
}
|
||||||
|
|
||||||
|
val connectionInfo = uri.pathSegments[0]
|
||||||
|
val json =
|
||||||
|
Base64.decode(connectionInfo, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
.toString(Charsets.UTF_8)
|
||||||
|
val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json)
|
||||||
|
val tcpService = networkConfig.services.first { v -> v.type == 0 }
|
||||||
|
|
||||||
|
val foundInfo = addRememberedDevice(
|
||||||
|
CastingDeviceInfo(
|
||||||
|
name = networkConfig.name,
|
||||||
|
type = CastProtocolType.FCAST,
|
||||||
|
addresses = networkConfig.addresses.toTypedArray(),
|
||||||
|
port = tcpService.port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
connectDevice(deviceFromInfo(foundInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
val ad = activeDevice ?: return;
|
||||||
|
_resumeCastingDevice = ad.getDeviceInfo()
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
|
||||||
|
Logger.i(TAG, "Stopping active device because of onStop.");
|
||||||
|
ad.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun start(context: Context) {
|
||||||
|
if (_started)
|
||||||
|
return;
|
||||||
|
_started = true;
|
||||||
|
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set null start")
|
||||||
|
_resumeCastingDevice = null;
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService starting...");
|
||||||
|
|
||||||
|
_castServer.start();
|
||||||
|
enableDeveloper(true);
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService started.");
|
||||||
|
|
||||||
|
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||||
|
startDiscovering()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun startDiscovering() {
|
||||||
|
_nsdManager?.apply {
|
||||||
|
_discoveryListeners.forEach {
|
||||||
|
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun stopDiscovering() {
|
||||||
|
_nsdManager?.apply {
|
||||||
|
_discoveryListeners.forEach {
|
||||||
|
try {
|
||||||
|
stopServiceDiscovery(it.value)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun stop() {
|
||||||
|
if (!_started)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_started = false;
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
|
stopDiscovering()
|
||||||
|
_scopeIO.cancel();
|
||||||
|
_scopeMain.cancel();
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopping active device because StateCasting is being stopped.")
|
||||||
|
val d = activeDevice;
|
||||||
|
activeDevice = null;
|
||||||
|
d?.disconnect();
|
||||||
|
|
||||||
|
_castServer.stop();
|
||||||
|
_castServer.removeAllHandlers();
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopped.")
|
||||||
|
|
||||||
|
_nsdManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDiscoveryListener(addOrUpdate: (String, Array<InetAddress>, Int) -> Unit): NsdManager.DiscoveryListener {
|
||||||
|
return object : NsdManager.DiscoveryListener {
|
||||||
|
override fun onDiscoveryStarted(regType: String) {
|
||||||
|
Log.d(TAG, "Service discovery started for $regType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(serviceType: String) {
|
||||||
|
Log.i(TAG, "Discovery stopped: $serviceType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost(service: NsdServiceInfo) {
|
||||||
|
Log.e(TAG, "service lost: $service")
|
||||||
|
// TODO: Handle service lost, e.g., remove device
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode")
|
||||||
|
try {
|
||||||
|
_nsdManager?.stopServiceDiscovery(this)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode")
|
||||||
|
try {
|
||||||
|
_nsdManager?.stopServiceDiscovery(this)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceFound(service: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "Service discovery success for ${service.serviceType}: $service")
|
||||||
|
val addresses = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
service.hostAddresses.toTypedArray()
|
||||||
|
} else {
|
||||||
|
arrayOf(service.host)
|
||||||
|
}
|
||||||
|
addOrUpdate(service.serviceName, addresses, service.port)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
_nsdManager?.registerServiceInfoCallback(
|
||||||
|
service,
|
||||||
|
{ it.run() },
|
||||||
|
object : NsdManager.ServiceInfoCallback {
|
||||||
|
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "onServiceUpdated: $serviceInfo")
|
||||||
|
addOrUpdate(
|
||||||
|
serviceInfo.serviceName,
|
||||||
|
serviceInfo.hostAddresses.toTypedArray(),
|
||||||
|
serviceInfo.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost() {
|
||||||
|
Log.v(TAG, "onServiceLost: $service")
|
||||||
|
// TODO: Handle service lost
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
|
||||||
|
Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceInfoCallbackUnregistered() {
|
||||||
|
Log.v(TAG, "onServiceInfoCallbackUnregistered")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_nsdManager?.resolveService(service, object : NsdManager.ResolveListener {
|
||||||
|
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||||
|
Log.v(TAG, "Resolve failed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "Resolve Succeeded: $serviceInfo")
|
||||||
|
addOrUpdate(
|
||||||
|
serviceInfo.serviceName,
|
||||||
|
arrayOf(serviceInfo.host),
|
||||||
|
serviceInfo.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startUpdateTimeJob(
|
||||||
|
onTimeJobTimeChanged_s: Event1<Long>,
|
||||||
|
setTime: (Long) -> Unit
|
||||||
|
): Job? {
|
||||||
|
val d = activeDevice;
|
||||||
|
if (d is CastingDeviceLegacyWrapper && (d.inner is AirPlayCastingDevice || d.inner is ChromecastCastingDevice)) {
|
||||||
|
return _scopeMain.launch {
|
||||||
|
while (true) {
|
||||||
|
val device = instance.activeDevice
|
||||||
|
if (device == null || !device.isPlaying) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1000)
|
||||||
|
val time_ms = (device.expectedCurrentTime * 1000.0).toLong()
|
||||||
|
setTime(time_ms)
|
||||||
|
onTimeJobTimeChanged_s.emit(device.expectedCurrentTime.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
||||||
|
return CastingDeviceLegacyWrapper(
|
||||||
|
when (deviceInfo.type) {
|
||||||
|
CastProtocolType.CHROMECAST -> {
|
||||||
|
ChromecastCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
CastProtocolType.AIRPLAY -> {
|
||||||
|
AirPlayCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
CastProtocolType.FCAST -> {
|
||||||
|
FCastCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateChromeCastDevice(
|
||||||
|
name: String,
|
||||||
|
addresses: Array<InetAddress>,
|
||||||
|
port: Int
|
||||||
|
) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = {
|
||||||
|
CastingDeviceLegacyWrapper(
|
||||||
|
ChromecastCastingDevice(
|
||||||
|
name,
|
||||||
|
addresses,
|
||||||
|
port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is ChromecastCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(d.inner.addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
d.inner.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateAirPlayDevice(name: String, addresses: Array<InetAddress>, port: Int) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = {
|
||||||
|
CastingDeviceLegacyWrapper(
|
||||||
|
AirPlayCastingDevice(
|
||||||
|
name,
|
||||||
|
addresses,
|
||||||
|
port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is AirPlayCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.port = port;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateFastCastDevice(name: String, addresses: Array<InetAddress>, port: Int) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = { CastingDeviceLegacyWrapper(FCastCastingDevice(name, addresses, port)) },
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is FCastCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.port = port;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun addOrUpdateCastDevice(
|
||||||
|
name: String,
|
||||||
|
deviceFactory: () -> CastingDevice,
|
||||||
|
deviceUpdater: (device: CastingDevice) -> Boolean
|
||||||
|
) {
|
||||||
|
var invokeEvents: (() -> Unit)? = null;
|
||||||
|
|
||||||
|
synchronized(devices) {
|
||||||
|
val device = devices[name];
|
||||||
|
if (device != null) {
|
||||||
|
val changed = deviceUpdater(device);
|
||||||
|
if (changed) {
|
||||||
|
invokeEvents = {
|
||||||
|
onDeviceChanged.emit(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val newDevice = deviceFactory();
|
||||||
|
this.devices[name] = newDevice
|
||||||
|
|
||||||
|
invokeEvents = {
|
||||||
|
onDeviceAdded.emit(newDevice);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeEvents?.let { _scopeMain.launch { it(); }; };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class FCastNetworkConfig(
|
||||||
|
val name: String,
|
||||||
|
val addresses: List<String>,
|
||||||
|
val services: List<FCastService>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class FCastService(
|
||||||
|
val port: Int,
|
||||||
|
val type: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "StateCastingLegacy"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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? {
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import android.view.View
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.CastProtocolType
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||||
@@ -38,7 +40,13 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_buttonConfirm = findViewById(R.id.button_confirm);
|
_buttonConfirm = findViewById(R.id.button_confirm);
|
||||||
_buttonTutorial = findViewById(R.id.button_tutorial)
|
_buttonTutorial = findViewById(R.id.button_tutorial)
|
||||||
|
|
||||||
ArrayAdapter.createFromResource(context, R.array.casting_device_type_array, R.layout.spinner_item_simple).also { adapter ->
|
val deviceTypeArray = if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
R.array.exp_casting_device_type_array
|
||||||
|
} else {
|
||||||
|
R.array.casting_device_type_array
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayAdapter.createFromResource(context, deviceTypeArray, R.layout.spinner_item_simple).also { adapter ->
|
||||||
adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
_spinnerType.adapter = adapter;
|
_spinnerType.adapter = adapter;
|
||||||
};
|
};
|
||||||
@@ -101,12 +109,16 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_textError.visibility = View.GONE;
|
_textError.visibility = View.GONE;
|
||||||
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
||||||
StateCasting.instance.addRememberedDevice(castingDeviceInfo);
|
try {
|
||||||
|
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to add remembered device: $e")
|
||||||
|
}
|
||||||
performDismiss();
|
performDismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonTutorial.setOnClickListener {
|
_buttonTutorial.setOnClickListener {
|
||||||
UIDialogs.showCastingTutorialDialog(context)
|
UIDialogs.showCastingTutorialDialog(context, ownerActivity)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +142,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -18,7 +17,6 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -83,7 +81,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();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,15 +106,16 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
synchronized(StateCasting.instance.devices) {
|
synchronized(StateCasting.instance.devices) {
|
||||||
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||||
|
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
|
|
||||||
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||||
val name = d.name
|
val name = d.name
|
||||||
if (name != null)
|
if (name != null) {
|
||||||
_devices.add(name)
|
_devices.add(name)
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||||
@@ -139,9 +138,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() {
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.FCastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -69,18 +68,18 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_buttonPlay = findViewById(R.id.button_play);
|
_buttonPlay = findViewById(R.id.button_play);
|
||||||
_buttonPlay.setOnClickListener {
|
_buttonPlay.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.resumeVideo()
|
StateCasting.instance.resumeVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonPause = findViewById(R.id.button_pause);
|
_buttonPause = findViewById(R.id.button_pause);
|
||||||
_buttonPause.setOnClickListener {
|
_buttonPause.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.pauseVideo()
|
StateCasting.instance.pauseVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonStop = findViewById(R.id.button_stop);
|
_buttonStop = findViewById(R.id.button_stop);
|
||||||
_buttonStop.setOnClickListener {
|
_buttonStop.setOnClickListener {
|
||||||
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||||
StateCasting.instance.activeDevice?.stopVideo()
|
StateCasting.instance.stopVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonNext = findViewById(R.id.button_next);
|
_buttonNext = findViewById(R.id.button_next);
|
||||||
@@ -90,7 +89,11 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_buttonClose.setOnClickListener { dismiss(); };
|
_buttonClose.setOnClickListener { dismiss(); };
|
||||||
_buttonDisconnect.setOnClickListener {
|
_buttonDisconnect.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.stopCasting();
|
try {
|
||||||
|
StateCasting.instance.activeDevice?.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Active device failed to disconnect: $e")
|
||||||
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,12 +102,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
StateCasting.instance.videoSeekTo(value.toDouble())
|
||||||
try {
|
|
||||||
activeDevice.seekVideo(value.toDouble());
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to change volume.", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: Check if volume slider is properly hidden in all cases
|
//TODO: Check if volume slider is properly hidden in all cases
|
||||||
@@ -113,14 +111,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
StateCasting.instance.changeVolume(value.toDouble())
|
||||||
if (activeDevice.canSetVolume) {
|
|
||||||
try {
|
|
||||||
activeDevice.changeVolume(value.toDouble());
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to change volume.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -172,15 +163,25 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
private fun updateDevice() {
|
private fun updateDevice() {
|
||||||
val d = StateCasting.instance.activeDevice ?: return;
|
val d = StateCasting.instance.activeDevice ?: return;
|
||||||
|
|
||||||
if (d is ChromecastCastingDevice) {
|
when (d.protocolType) {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
CastProtocolType.CHROMECAST -> {
|
||||||
_textType.text = "Chromecast";
|
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||||
} else if (d is AirPlayCastingDevice) {
|
_textType.text = "Chromecast";
|
||||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
}
|
||||||
_textType.text = "AirPlay";
|
CastProtocolType.AIRPLAY -> {
|
||||||
} else if (d is FCastCastingDevice) {
|
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
_textType.text = "AirPlay";
|
||||||
_textType.text = "FastCast";
|
}
|
||||||
|
CastProtocolType.FCAST -> {
|
||||||
|
_imageDevice.setImageResource(
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
R.drawable.ic_exp_fc
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_fc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_textType.text = "FCast";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_textName.text = d.name;
|
_textName.text = d.name;
|
||||||
@@ -192,7 +193,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||||
_sliderPosition.valueTo = dur
|
_sliderPosition.valueTo = dur
|
||||||
|
|
||||||
if (d.canSetVolume) {
|
if (d.canSetVolume()) {
|
||||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||||
_layoutVolumeFixed.visibility = View.GONE;
|
_layoutVolumeFixed.visibility = View.GONE;
|
||||||
} else {
|
} else {
|
||||||
@@ -214,8 +215,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
CastConnectionState.CONNECTED -> {
|
CastConnectionState.CONNECTED -> {
|
||||||
enableControls(interactiveControls)
|
enableControls(interactiveControls)
|
||||||
}
|
}
|
||||||
CastConnectionState.CONNECTING,
|
CastConnectionState.CONNECTING, CastConnectionState.DISCONNECTED -> {
|
||||||
CastConnectionState.DISCONNECTED -> {
|
|
||||||
disableControls(interactiveControls)
|
disableControls(interactiveControls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,18 +6,19 @@ 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
|
||||||
import com.futo.platformplayer.engine.exceptions.NoInternetException
|
import com.futo.platformplayer.engine.exceptions.NoInternetException
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCompilationException
|
import com.futo.platformplayer.engine.exceptions.ScriptCompilationException
|
||||||
@@ -26,6 +27,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
|
||||||
@@ -35,11 +37,22 @@ import com.futo.platformplayer.engine.packages.PackageHttp
|
|||||||
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||||
import com.futo.platformplayer.engine.packages.V8Package
|
import com.futo.platformplayer.engine.packages.V8Package
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
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 +60,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 +84,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 +155,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 +164,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 +202,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 +218,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 +233,149 @@ 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();
|
||||||
|
//Logger.i(TAG, "Busy Enter [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
|
||||||
|
try {
|
||||||
|
return handle();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
//Logger.i(TAG, "Busy Leave [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
|
||||||
|
_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 +401,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 +442,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 +472,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) {
|
||||||
@@ -356,18 +515,29 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
||||||
|
throw getExceptionFromPlugin(config, pluginType, msg, innerEx, stack, code);
|
||||||
|
}
|
||||||
|
fun getExceptionFromPlugin(config: IV8PluginConfig, obj: V8ValueObject, innerEx: Exception? = null, stack: String? = null, code: String? = null, prefix: String? = null): PluginException {
|
||||||
|
val pluginType = obj.getOrDefault(config, "plugin_type", "Exception Handling", "")?.let { if(!it.isNullOrBlank()) it + "" else "" } ?: "";
|
||||||
|
var msg = obj.getOrDefault<String?>(config, "msg", "Exception Handling", null)
|
||||||
|
?: obj.getOrDefault(config, "message", "Exception Handling", "");
|
||||||
|
if(!prefix.isNullOrBlank())
|
||||||
|
msg = prefix + msg;
|
||||||
|
return getExceptionFromPlugin(config, pluginType, msg ?: "Unknown exception", innerEx, stack, code);
|
||||||
|
}
|
||||||
|
fun getExceptionFromPlugin(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null): PluginException {
|
||||||
when(pluginType) {
|
when(pluginType) {
|
||||||
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
|
"ScriptException" -> return ScriptException(config, msg, innerEx, stack, code);
|
||||||
"CriticalException" -> throw ScriptCriticalException(config, msg, innerEx, stack, code);
|
"CriticalException" -> return ScriptCriticalException(config, msg, innerEx, stack, code);
|
||||||
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
|
"AgeException" -> return ScriptAgeException(config, msg, innerEx, stack, code);
|
||||||
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
|
"UnavailableException" -> return ScriptUnavailableException(config, msg, innerEx, stack, code);
|
||||||
"ScriptLoginRequiredException" -> throw ScriptLoginRequiredException(config, msg, innerEx, stack, code);
|
"ScriptLoginRequiredException" -> return ScriptLoginRequiredException(config, msg, innerEx, stack, code);
|
||||||
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
"ScriptExecutionException" -> return ScriptExecutionException(config, msg, innerEx, stack, code);
|
||||||
"ScriptCompilationException" -> throw ScriptCompilationException(config, msg, innerEx, code);
|
"ScriptCompilationException" -> return ScriptCompilationException(config, msg, innerEx, code);
|
||||||
"ScriptImplementationException" -> throw ScriptImplementationException(config, msg, innerEx, null, code);
|
"ScriptImplementationException" -> return ScriptImplementationException(config, msg, innerEx, null, code);
|
||||||
"ScriptTimeoutException" -> throw ScriptTimeoutException(config, msg, innerEx);
|
"ScriptTimeoutException" -> return ScriptTimeoutException(config, msg, innerEx);
|
||||||
"NoInternetException" -> throw NoInternetException(config, msg, innerEx, stack, code);
|
"NoInternetException" -> return NoInternetException(config, msg, innerEx, stack, code);
|
||||||
else -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
else -> return ScriptExecutionException(config, msg, innerEx, stack, code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,9 +568,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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user