Compare commits

..

No commits in common. "master" and "292" have entirely different histories.
master ... 292

284 changed files with 4554 additions and 10808 deletions

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
aar/* filter=lfs diff=lfs merge=lfs -text
app/aar/* filter=lfs diff=lfs merge=lfs -text

View file

@ -1,9 +1,6 @@
name: Bug Report name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior. description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["Bug", "Android"] labels: ["Bug"]
title: "Bug: "
type: bug
projects: ["futo-org/19"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -21,33 +18,11 @@ body:
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown * if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea - type: textarea
id: reproduction-steps id: what-happened
attributes: attributes:
label: Reproduction steps label: What happened?
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: What did you expect to happen?
placeholder: | placeholder: Tell us what you see!
0. Play a Youtube video
1. Press on Download button
2. Select quality 1440p
3. Grayjay crashes when attempting to download
validations:
required: true
- type: textarea
id: actual-result
attributes:
label: Actual result
description: What happend?
placeholder: Tell us what you saw!
validations:
required: true
- type: textarea
id: expected-result
attributes:
label: Expected result
description: What was suppose to happen?
placeholder: Tell us what you expected to happen!
validations: validations:
required: true required: true
@ -56,7 +31,7 @@ body:
attributes: attributes:
label: Grayjay Version label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name". description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "311" placeholder: "242"
validations: validations:
required: true required: true
@ -67,23 +42,19 @@ body:
multiple: true multiple: true
options: options:
- "All" - "All"
- "Apple Podcasts" - "Youtube"
- "Odysee"
- "Rumble"
- "Kick"
- "Twitch"
- "PeerTube"
- "Patreon"
- "Nebula"
- "BiliBili (CN)" - "BiliBili (CN)"
- "Bitchute" - "Bitchute"
- "Crunchyroll"
- "CuriosityStream"
- "Dailymotion"
- "Kick"
- "Nebula"
- "Odysee"
- "Patreon"
- "PeerTube"
- "Rumble"
- "SoundCloud" - "SoundCloud"
- "Spotify" - "Dailymotion"
- "TedTalks" - "Apple Podcasts"
- "Twitch"
- "Youtube"
- "Other" - "Other"
validations: validations:
required: true required: true
@ -95,30 +66,6 @@ body:
description: In the application, select Sources > [the broken plugin], write down the value under "Version". description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12" placeholder: "12"
- type: input
id: android-version
attributes:
label: Which android version are you using?
placeholder: "Android 15"
validations:
required: true
- type: input
id: phone-model
attributes:
label: Which device are you using?
placeholder: "Google Pixel 9"
validations:
required: true
- type: input
id: os-version
attributes:
label: Which operating system are you using?
placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..."
validations:
required: true
- type: checkboxes - type: checkboxes
id: login id: login
attributes: attributes:
@ -139,28 +86,9 @@ body:
validations: validations:
required: true required: true
- type: textarea
id: grayjay-references
attributes:
label: References
description: |
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above? If so, please create a list below that mentions each of them. For example:
```
- #10
```
placeholder:
value:
validations:
required: false
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
label: Relevant log output label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell render: shell
- type: markdown
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.

View file

@ -1,16 +1,13 @@
name: Documentation Issue name: Documentation Issue
description: Report an issue or suggest a change in the documentation. description: Report an issue or suggest a change in the documentation.
labels: ["Documentation"] labels: ["Documentation"]
title: "Documentation: "
type: task
projects: ["futo-org/19"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# Thank you for opening a documentation change request. # Thank you for opening a documentation change request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app) The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention. Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay) For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)

View file

@ -1,16 +1,13 @@
name: Feature Request name: Feature Request
description: Suggest a new feature or other enhancement. description: Suggest a new feature or other enhancement.
labels: ["Enhancement", "Android"] labels: ["Enhancement"]
title: "Feature request: "
type: feature
projects: ["futo-org/19"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# Thank you for opening a feature request. # Thank you for opening a feature request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay) For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)

12
.gitmodules vendored
View file

@ -94,15 +94,3 @@
[submodule "app/src/unstable/assets/sources/tedtalks"] [submodule "app/src/unstable/assets/sources/tedtalks"]
path = app/src/unstable/assets/sources/tedtalks path = app/src/unstable/assets/sources/tedtalks
url = ../plugins/tedtalks.git url = ../plugins/tedtalks.git
[submodule "app/src/stable/assets/sources/curiositystream"]
path = app/src/stable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/curiositystream"]
path = app/src/unstable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/crunchyroll"]
path = app/src/unstable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git
[submodule "app/src/stable/assets/sources/crunchyroll"]
path = app/src/stable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git

BIN
app/aar/ffmpeg-kit-full-6.0-2.LTS.aar (Stored with Git LFS)

Binary file not shown.

View file

@ -180,7 +180,6 @@ 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'
@ -198,8 +197,7 @@ dependencies {
implementation 'org.jsoup:jsoup:1.15.3' implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation fileTree(dir: 'aar', include: ['*.aar']) implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
implementation 'com.arthenica:smart-exception-java:0.2.1'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0' implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1' implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1' implementation 'com.google.zxing:core:3.4.1'

View file

@ -1,38 +0,0 @@
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()
)
}
}

View file

@ -1,338 +0,0 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.select
import org.junit.Assert.*
import org.junit.Test
import java.net.Socket
import java.nio.ByteBuffer
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/*
class SyncServerTests {
//private val relayHost = "relay.grayjay.app"
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
private val relayHost = "192.168.1.138"
private val relayPort = 9000
/** Creates a client connected to the live relay server. */
private suspend fun createClient(
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
isHandshakeAllowed: ((LinkType, SyncSocketSession, String, String?, UInt) -> Boolean)? = null,
onException: ((Throwable) -> Unit)? = null
): SyncSocketSession = withContext(Dispatchers.IO) {
val p = Noise.createDH("25519")
p.generateKeyPair()
val socket = Socket(relayHost, relayPort)
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
val tcs = CompletableDeferred<Boolean>()
val socketSession = SyncSocketSession(
relayHost,
p,
inputStream,
outputStream,
onClose = { socket.close() },
onHandshakeComplete = { s ->
onHandshakeComplete?.invoke(s)
tcs.complete(true)
},
onData = onData ?: { _, _, _, _ -> },
onNewChannel = onNewChannel ?: { _, _ -> },
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _, _, _ -> true }
)
socketSession.authorizable = AlwaysAuthorized()
try {
socketSession.startAsInitiator(relayKey)
} catch (e: Throwable) {
onException?.invoke(e)
}
withTimeout(5000.milliseconds) { tcs.await() }
return@withContext socketSession
}
@Test
fun multipleClientsHandshake_Success() = runBlocking {
val client1 = createClient()
val client2 = createClient()
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
client1.stop()
client2.stop()
}
@Test
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
delay(100.milliseconds)
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
assertNotNull("Client B should receive connection info", infoB)
assertEquals(12345.toUShort(), infoB!!.port)
assertNull("Client C should not receive connection info (unauthorized)", infoC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun relayedTransport_Bidirectional_Success() = runBlocking {
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
val tcsDataA = CompletableDeferred<ByteArray>()
channelA.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
}
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
assertArrayEquals(maxSizeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val clientC = createClient()
val data = byteArrayOf(1, 2, 3)
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
assertTrue(success)
assertNotNull(recordB)
assertArrayEquals(data, recordB!!.first)
assertNull("Unauthorized client should not access record", recordC)
clientA.stop()
clientB.stop()
clientC.stop()
}
@Test
fun getNonExistentRecord_ReturnsNull() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
assertNull("Getting non-existent record should return null", record)
clientA.stop()
clientB.stop()
}
@Test
fun updateRecord_TimestampUpdated() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val key = "updateKey"
val data1 = byteArrayOf(1)
val data2 = byteArrayOf(2)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
val record1 = clientB.getRecord(clientA.localPublicKey, key)
delay(1000.milliseconds)
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
val record2 = clientB.getRecord(clientA.localPublicKey, key)
assertNotNull(record1)
assertNotNull(record2)
assertTrue(record2!!.second > record1!!.second)
assertArrayEquals(data2, record2.first)
clientA.stop()
clientB.stop()
}
@Test
fun deleteRecord_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val data = byteArrayOf(1, 2, 3)
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
assertTrue(success)
assertNull(record)
clientA.stop()
clientB.stop()
}
@Test
fun listRecordKeys_Success() = runBlocking {
val clientA = createClient()
val clientB = createClient()
val keys = arrayOf("key1", "key2", "key3")
keys.forEach { key ->
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
}
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
clientA.stop()
clientB.stop()
}
@Test
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
val tcsA = CompletableDeferred<ChannelRelayed>()
val tcsB = CompletableDeferred<ChannelRelayed>()
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
channelA.authorizable = AlwaysAuthorized()
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
channelB.authorizable = AlwaysAuthorized()
channelTask.await()
val tcsDataB = CompletableDeferred<ByteArray>()
channelB.setDataHandler { _, _, o, so, d ->
val b = ByteArray(d.remaining())
d.get(b)
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
}
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
assertArrayEquals(largeData, receivedData)
clientA.stop()
clientB.stop()
}
@Test
fun publishAndGetLargeRecord_Success() = runBlocking {
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
val clientA = createClient()
val clientB = createClient()
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
assertTrue(success)
assertNotNull(record)
assertArrayEquals(largeData, record!!.first)
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithValidAppId_Success() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId }
)
val clientA = createClient()
// Act: Start relayed channel with valid appId
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey, appId = allowedAppId) }
val channelB = withTimeout(5.seconds) { tcsB.await() }
withTimeout(5.seconds) { channelTask.await() }
// Assert: Channel is established
assertNotNull("Channel should be created on target with valid appId", channelB)
// Clean up
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithInvalidAppId_Fails() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val invalidAppId = 5678u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId },
onException = { }
)
val clientA = createClient()
// Act & Assert: Attempt with invalid appId should fail
try {
withTimeout(5.seconds) {
clientA.startRelayedChannel(clientB.localPublicKey, appId = invalidAppId)
}
fail("Starting relayed channel with invalid appId should fail")
} catch (e: Throwable) {
// Expected: The channel creation should time out or fail
}
// Ensure no channel was created on client B
val completedTask = select {
tcsB.onAwait { "channel" }
async { delay(1.seconds); "timeout" }.onAwait { "timeout" }
}
assertEquals("No channel should be created with invalid appId", "timeout", completedTask)
// Clean up
clientA.stop()
clientB.stop()
}
}
class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true
}*/

View file

@ -1,512 +0,0 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import org.junit.Assert.*
import org.junit.Test
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.ByteBuffer
import kotlin.random.Random
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.Duration.Companion.seconds
/*
data class PipeStreams(
val initiatorInput: LittleEndianDataInputStream,
val initiatorOutput: LittleEndianDataOutputStream,
val responderInput: LittleEndianDataInputStream,
val responderOutput: LittleEndianDataOutputStream
)
typealias OnHandshakeComplete = (SyncSocketSession) -> Unit
typealias IsHandshakeAllowed = (LinkType, SyncSocketSession, String, String?, UInt) -> Boolean
typealias OnClose = (SyncSocketSession) -> Unit
typealias OnData = (SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit
class SyncSocketTests {
private fun createPipeStreams(): PipeStreams {
val initiatorOutput = PipedOutputStream()
val responderOutput = PipedOutputStream()
val responderInput = PipedInputStream(initiatorOutput)
val initiatorInput = PipedInputStream(responderOutput)
return PipeStreams(
LittleEndianDataInputStream(initiatorInput), LittleEndianDataOutputStream(initiatorOutput),
LittleEndianDataInputStream(responderInput), LittleEndianDataOutputStream(responderOutput)
)
}
fun generateKeyPair(): DHState {
val p = Noise.createDH("25519")
p.generateKeyPair()
return p
}
private fun createSessions(
initiatorInput: LittleEndianDataInputStream,
initiatorOutput: LittleEndianDataOutputStream,
responderInput: LittleEndianDataInputStream,
responderOutput: LittleEndianDataOutputStream,
initiatorKeyPair: DHState,
responderKeyPair: DHState,
onInitiatorHandshakeComplete: OnHandshakeComplete,
onResponderHandshakeComplete: OnHandshakeComplete,
onInitiatorClose: OnClose? = null,
onResponderClose: OnClose? = null,
onClose: OnClose? = null,
isHandshakeAllowed: IsHandshakeAllowed? = null,
onDataA: OnData? = null,
onDataB: OnData? = null
): Pair<SyncSocketSession, SyncSocketSession> {
val initiatorSession = SyncSocketSession(
"", initiatorKeyPair, initiatorInput, initiatorOutput,
onClose = {
onClose?.invoke(it)
onInitiatorClose?.invoke(it)
},
onHandshakeComplete = onInitiatorHandshakeComplete,
onData = onDataA,
isHandshakeAllowed = isHandshakeAllowed
)
val responderSession = SyncSocketSession(
"", responderKeyPair, responderInput, responderOutput,
onClose = {
onClose?.invoke(it)
onResponderClose?.invoke(it)
},
onHandshakeComplete = onResponderHandshakeComplete,
onData = onDataB,
isHandshakeAllowed = isHandshakeAllowed
)
return Pair(initiatorSession, responderSession)
}
@Test
fun handshake_WithValidPairingCode_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = validPairingCode)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun handshake_WithInvalidPairingCode_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val invalidPairingCode = "wrong"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = invalidPairingCode)
responderSession.startAsResponder()
withTimeout(100.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithoutPairingCodeWhenRequired_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey) // No pairing code
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithPairingCodeWhenNotRequired_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val pairingCode = "unnecessary"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, _, _ -> true } // Always allow
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = pairingCode)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun sendAndReceive_SmallDataPacket_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val smallData = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(smallData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(smallData, receivedData)
}
@Test
fun sendAndReceive_ExactlyMaximumPacketSize_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val maxData = ByteArray(SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(maxData, receivedData)
}
@Test
fun stream_LargeData_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val largeData = ByteArray(2 * (SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE)).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(largeData, receivedData)
}
@Test
fun authorizedSession_CanSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize both sessions
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(data, receivedData)
}
@Test
fun unauthorizedSession_CannotSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, _, _, _ -> }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize initiator but not responder
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Unauthorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
delay(1.seconds)
assertFalse(tcsDataReceived.isCompleted)
}
@Test
fun directHandshake_WithValidAppId_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = allowedAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
assertNotNull(initiatorSession.remotePublicKey)
assertNotNull(responderSession.remotePublicKey)
}
@Test
fun directHandshake_WithInvalidAppId_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val invalidAppId = 5678u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = invalidAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
}
class Authorized : IAuthorizable {
override val isAuthorized: Boolean = true
}
class Unauthorized : IAuthorizable {
override val isAuthorized: Boolean = false
}*/

View file

@ -55,7 +55,7 @@
<activity <activity
android:name=".activities.MainActivity" android:name=".activities.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true" android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar" android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleInstance" android:launchMode="singleInstance"
@ -239,4 +239,4 @@
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
</application> </application>
</manifest> </manifest>

View file

@ -32,8 +32,7 @@ let Type = {
Text: { Text: {
RAW: 0, RAW: 0,
HTML: 1, HTML: 1,
MARKUP: 2, MARKUP: 2
CODE: 3
}, },
Chapter: { Chapter: {
NORMAL: 0, NORMAL: 0,
@ -103,12 +102,6 @@ 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);
@ -298,39 +291,15 @@ class PlatformPostDetails extends PlatformPost {
} }
} }
class PlatformWeb extends PlatformContent { class PlatformArticleDetails extends PlatformContent {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWeb";
}
}
class PlatformWebDetails extends PlatformWeb {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWebDetails";
this.html = obj.html;
}
}
class PlatformArticle extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticle";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class PlatformArticleDetails extends PlatformArticle {
constructor(obj) { constructor(obj) {
super(obj, 3); super(obj, 3);
obj = obj ?? {}; obj = obj ?? {};
this.plugin_type = "PlatformArticleDetails"; this.plugin_type = "PlatformArticleDetails";
this.rating = obj.rating ?? new RatingLikes(-1); this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.segments = obj.segments ?? []; this.segments = obj.segments ?? [];
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
} }
} }
class ArticleSegment { class ArticleSegment {
@ -346,17 +315,9 @@ class ArticleTextSegment extends ArticleSegment {
} }
} }
class ArticleImagesSegment extends ArticleSegment { class ArticleImagesSegment extends ArticleSegment {
constructor(images, caption) { constructor(images) {
super(2); super(2);
this.images = images; this.images = images;
this.caption = caption;
}
}
class ArticleHeaderSegment extends ArticleSegment {
constructor(content, level) {
super(3);
this.level = level;
this.content = content;
} }
} }
class ArticleNestedSegment extends ArticleSegment { class ArticleNestedSegment extends ArticleSegment {
@ -634,8 +595,6 @@ class PlatformComment {
this.date = obj.date ?? 0; this.date = obj.date ?? 0;
this.replyCount = obj.replyCount ?? 0; this.replyCount = obj.replyCount ?? 0;
this.context = obj.context ?? {}; this.context = obj.context ?? {};
if(obj.getReplies)
this.getReplies = obj.getReplies;
} }
} }
@ -707,12 +666,11 @@ class LiveEventViewCount extends LiveEvent {
} }
} }
class LiveEventRaid extends LiveEvent { class LiveEventRaid extends LiveEvent {
constructor(targetUrl, targetName, targetThumbnail, isOutgoing) { constructor(targetUrl, targetName, targetThumbnail) {
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;
} }
} }

View file

@ -1,319 +0,0 @@
import kotlin.math.*
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
init {
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
"RGBA channels must be in [0,1]"
}
}
// -- RGB(A) channels stored 01 --
var r: Float = r.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var g: Float = g.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var b: Float = b.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var a: Float = a.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f) }
// -- Int views of RGBA 0255 --
var red: Int
get() = (r * 255).roundToInt()
set(v) { r = (v.coerceIn(0, 255) / 255f) }
var green: Int
get() = (g * 255).roundToInt()
set(v) { g = (v.coerceIn(0, 255) / 255f) }
var blue: Int
get() = (b * 255).roundToInt()
set(v) { b = (v.coerceIn(0, 255) / 255f) }
var alpha: Int
get() = (a * 255).roundToInt()
set(v) { a = (v.coerceIn(0, 255) / 255f) }
// -- HSLA storage & lazy recompute flags --
private var _h: Float = 0f
private var _s: Float = 0f
private var _l: Float = 0f
private var _hslDirty = true
/** Hue [0...360) */
var hue: Float
get() { computeHslIfNeeded(); return _h }
set(v) { setHsl(v, saturation, lightness) }
/** Saturation [0...1] */
var saturation: Float
get() { computeHslIfNeeded(); return _s }
set(v) { setHsl(hue, v, lightness) }
/** Lightness [0...1] */
var lightness: Float
get() { computeHslIfNeeded(); return _l }
set(v) { setHsl(hue, saturation, v) }
private fun computeHslIfNeeded() {
if (!_hslDirty) return
val max = max(max(r, g), b)
val min = min(min(r, g), b)
val d = max - min
_l = (max + min) / 2f
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
_h = when {
d == 0f -> 0f
max == r -> ((g - b) / d % 6f) * 60f
max == g -> (((b - r) / d) + 2f) * 60f
else -> (((r - g) / d) + 4f) * 60f
}.let { if (it < 0f) it + 360f else it }
_hslDirty = false
}
/**
* Set all three HSL channels at once.
* Hue in degrees [0...360), s/l [0...1].
*/
fun setHsl(h: Float, s: Float, l: Float) {
val hh = ((h % 360f) + 360f) % 360f
val cc = (1f - abs(2f * l - 1f)) * s
val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
val m = l - cc / 2f
val (rp, gp, bp) = when {
hh < 60f -> Triple(cc, x, 0f)
hh < 120f -> Triple(x, cc, 0f)
hh < 180f -> Triple(0f, cc, x)
hh < 240f -> Triple(0f, x, cc)
hh < 300f -> Triple(x, 0f, cc)
else -> Triple(cc, 0f, x)
}
r = rp + m; g = gp + m; b = bp + m
_h = hh; _s = s; _l = l; _hslDirty = false
}
/** Return 0xRRGGBBAA int */
fun toRgbaInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
}
/** Return 0xAARRGGBB int */
fun toArgbInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
}
// — Convenience modifiers (chainable) —
/** Lighten by fraction [0...1] */
fun lighten(fraction: Float): CSSColor = apply {
lightness = (lightness + fraction).coerceIn(0f, 1f)
}
/** Darken by fraction [0...1] */
fun darken(fraction: Float): CSSColor = apply {
lightness = (lightness - fraction).coerceIn(0f, 1f)
}
/** Increase saturation by fraction [0...1] */
fun saturate(fraction: Float): CSSColor = apply {
saturation = (saturation + fraction).coerceIn(0f, 1f)
}
/** Decrease saturation by fraction [0...1] */
fun desaturate(fraction: Float): CSSColor = apply {
saturation = (saturation - fraction).coerceIn(0f, 1f)
}
/** Rotate hue by degrees (can be negative) */
fun rotateHue(degrees: Float): CSSColor = apply {
hue = (hue + degrees) % 360f
}
companion object {
/** Create from Android 0xAARRGGBB */
@JvmStatic fun fromArgb(color: Int): CSSColor {
val a = ((color ushr 24) and 0xFF) / 255f
val r = ((color ushr 16) and 0xFF) / 255f
val g = ((color ushr 8) and 0xFF) / 255f
val b = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
/** Create from Android 0xRRGGBBAA */
@JvmStatic fun fromRgba(color: Int): CSSColor {
val r = ((color ushr 24) and 0xFF) / 255f
val g = ((color ushr 16) and 0xFF) / 255f
val b = ((color ushr 8) and 0xFF) / 255f
val a = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
@JvmStatic fun fromAndroidColor(color: Int): CSSColor {
return fromArgb(color)
}
private val NAMED_HEX = mapOf(
"aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
"aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
"bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
"blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
"burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
"chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
"cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
"darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
"darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
"darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
"darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
"darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
"darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
"darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
"dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
"firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
"fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
"gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
"green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
"honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
"indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
"lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
"lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
"lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
"lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
"lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
"lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
"lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
"linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
"mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
"mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
"mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
"midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
"moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
"oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
"orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
"palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
"palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
"peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
"powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
"red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
"saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
"seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
"silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
"slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
"springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
"teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
"turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
"white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
"yellowgreen" to "9ACD32"
)
private val NAMED: Map<String, Int> = NAMED_HEX
.mapValues { (_, hexRgb) ->
// parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
val rgb = hexRgb.toInt(16)
(rgb shl 8) or 0xFF
} + ("transparent" to 0x00000000)
private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
@JvmStatic
fun parseColor(s: String): CSSColor {
val str = s.trim()
// named
NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
// hex
HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
return parseHexPart(part)
}
// rgb/rgba
RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseRgbParts(it.split(',').map(String::trim))
}
// hsl/hsla
HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseHslParts(it.split(',').map(String::trim))
}
error("Cannot parse color: \"$s\"")
}
private fun parseHexPart(p: String): CSSColor {
// expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
val hex = when (p.length) {
3 -> p.map { "$it$it" }.joinToString("") + "FF"
4 -> p.map { "$it$it" }.joinToString("")
6 -> p + "FF"
8 -> p
else -> error("Invalid hex color: #$p")
}
val parsed = hex.toLong(16).toInt()
val alpha = (parsed and 0xFF) shl 24
val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
val argb = alpha or rgbOnly
return fromArgb(argb)
}
private fun parseRgbParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
// r/g/b: "128" → 128/255, "50%" → 0.5
fun channel(ch: String): Float =
if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
else ch.toFloat().coerceIn(0f, 255f) / 255f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
else a.toFloat().coerceIn(0f, 1f)
val r = channel(parts[0])
val g = channel(parts[1])
val b = channel(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(r, g, b, a)
}
private fun parseHslParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
fun hueOf(h: String): Float = when {
h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
else -> h.toFloat()
}
// for s and l you only ever see percentages
fun pct(p: String): Float =
p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) pct(a)
else a.toFloat().coerceIn(0f, 1f)
val h = hueOf(parts[0])
val s = pct(parts[1])
val l = pct(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
}
}
}
fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
fun CSSColor.toAndroidColor(): Int = toArgbInt()

View file

@ -14,6 +14,7 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong import kotlin.math.roundToLong
@ -375,19 +376,14 @@ private val slds = hashSetOf(".com.ac", ".net.ac", ".gov.ac", ".org.ac", ".mil.a
fun String.matchesDomain(queryDomain: String): Boolean { fun String.matchesDomain(queryDomain: String): Boolean {
if(queryDomain.startsWith(".")) { if(queryDomain.startsWith(".")) {
val parts = this.lowercase().split(".");
val queryParts = queryDomain.lowercase().trimStart("."[0]).split("."); val parts = queryDomain.lowercase().split(".");
if(queryParts.size < 2) if(parts.size < 3)
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")"); throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
else { if(parts.size >= 3){
val possibleDomain = "." + queryParts.joinToString("."); val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
if(slds.contains(possibleDomain)) if(isSLD && parts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")"); throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
/*
val isSLD = slds.contains("." + queryParts[queryParts.size - 2] + "." + queryParts[queryParts.size - 1]);
if(isSLD && queryParts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
*/
} }
//TODO: Should be safe, but double verify if can't be exploited //TODO: Should be safe, but double verify if can't be exploited
@ -399,11 +395,9 @@ fun String.matchesDomain(queryDomain: String): Boolean {
fun String.getSubdomainWildcardQuery(): String { fun String.getSubdomainWildcardQuery(): String {
val domainParts = this.split("."); val domainParts = this.split(".");
var wildcardDomain = if(domainParts.size > 2) val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
"." + domainParts.drop(1).joinToString(".") if(slds.contains(sldParts))
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
else else
"." + domainParts.joinToString("."); return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
if(slds.contains(wildcardDomain.lowercase()))
"." + domainParts.joinToString(".");
return wildcardDomain;
} }

View file

@ -217,9 +217,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
} }
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? { fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
ensureNotMainThread() val timeout = 2000
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")})");
@ -241,11 +241,8 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
return null; return null;
} }
val sortedAddresses: List<InetAddress> = addresses
.sortedBy { addr -> addressScore(addr) }
val sockets: ArrayList<Socket> = arrayListOf(); val sockets: ArrayList<Socket> = arrayListOf();
for (i in sortedAddresses.indices) { for (i in addresses.indices) {
sockets.add(Socket()); sockets.add(Socket());
} }
@ -253,7 +250,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
var connectedSocket: Socket? = null; var connectedSocket: Socket? = null;
val threads: ArrayList<Thread> = arrayListOf(); val threads: ArrayList<Thread> = arrayListOf();
for (i in 0 until sockets.size) { for (i in 0 until sockets.size) {
val address = sortedAddresses[i]; val address = addresses[i];
val socket = sockets[i]; val socket = sockets[i];
val thread = Thread { val thread = Thread {
try { try {

View file

@ -7,9 +7,6 @@ import java.net.InetAddress
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.net.URLEncoder import java.net.URLEncoder
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
//Syntax sugaring //Syntax sugaring
inline fun <reified T> Any.assume(): T?{ inline fun <reified T> Any.assume(): T?{
@ -36,37 +33,13 @@ fun Boolean?.toYesNo(): String {
fun InetAddress?.toUrlAddress(): String { fun InetAddress?.toUrlAddress(): String {
return when (this) { return when (this) {
is Inet6Address -> { is Inet6Address -> {
val hostAddr = this.hostAddress ?: throw Exception("Invalid address: hostAddress is null") "[${hostAddress}]"
val index = hostAddr.indexOf('%')
if (index != -1) {
val addrPart = hostAddr.substring(0, index)
val scopeId = hostAddr.substring(index + 1)
"[${addrPart}%25${scopeId}]" // %25 is URL-encoded '%'
} else {
"[$hostAddr]"
}
} }
is Inet4Address -> { is Inet4Address -> {
this.hostAddress ?: throw Exception("Invalid address: hostAddress is null") hostAddress
} }
else -> { else -> {
throw Exception("Invalid address type") throw Exception("Invalid address type")
} }
} }
}
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
}
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
} }

View file

@ -2,30 +2,10 @@ package com.futo.platformplayer
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.* import com.caoccao.javet.values.primitive.*
import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.selects.SelectClause0
import kotlinx.coroutines.selects.SelectClause1
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
//V8 //V8
@ -44,10 +24,6 @@ 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}");
@ -113,29 +89,7 @@ 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 -> {
@ -192,119 +146,4 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() }) for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
map.put(prop, obj.getString(prop)); map.put(prop, obj.getString(prop));
return map; return map;
}
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
val latch = CountDownLatch(1);
var promiseResult: T? = null;
var promiseException: Throwable? = null;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
if(p0 is V8ValueError)
promiseException = ScriptExecutionException(plugin.config, p0.message);
else
promiseResult = p0 as T;
latch.countDown();
}
override fun onRejected(p0: V8Value?) {
promiseException = (NotImplementedError("onRejected promise not implemented.."));
latch.countDown();
}
override fun onCatch(p0: V8Value?) {
promiseException = (NotImplementedError("onCatch promise not implemented.."));
latch.countDown();
}
});
}
plugin.registerPromise(this) {
promiseException = CancellationException("Cancelled by system");
latch.countDown();
}
plugin.unbusy {
latch.await();
}
if(promiseException != null)
throw promiseException!!;
return promiseResult!!;
}
fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T> {
val underlyingDef = CompletableDeferred<T>();
val def = if(this.has("estDuration"))
V8Deferred(underlyingDef,
this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
else
V8Deferred<T>(underlyingDef);
val promise = this;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.complete(p0 as T);
}
override fun onRejected(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
}
override fun onCatch(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
}
});
}
plugin.registerPromise(promise) {
if(def.isActive)
def.cancel("Cancelled by system");
}
return def;
}
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
val newDef = CompletableDeferred<R>()
this.invokeOnCompletion {
if(it != null)
newDef.completeExceptionally(it);
else
newDef.complete(conversion(this@V8Deferred.getCompleted()));
}
return V8Deferred<R>(newDef, estDuration);
}
companion object {
fun <T, R> merge(scope: CoroutineScope, defs: List<V8Deferred<T>>, conversion: (result: List<T>)->R): V8Deferred<R> {
var amount = -1;
for(def in defs)
amount = Math.max(amount, def.estDuration);
val def = scope.async {
val results = defs.map { it.await() };
return@async conversion(results);
}
return V8Deferred(def, amount);
}
}
}
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any): T {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result as T;
}
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any): V8Deferred<T> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!);
}
return V8Deferred(CompletableDeferred(result as T));
} }

View file

@ -29,7 +29,6 @@ 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
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.AdvancedField
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId 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
@ -176,10 +175,6 @@ class Settings : FragmentedStorageFileJson() {
} }
}*/ }*/
@FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings")
var advancedSettings: Boolean = false;
@FormField(R.string.language, "group", -1, 0) @FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings(); var language = LanguageSettings();
@Serializable @Serializable
@ -226,11 +221,10 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5) @FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false; var showHomeFiltersPluginNames: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@ -259,11 +253,9 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.feed_style) @DropdownFieldOptionsId(R.array.feed_style)
var searchFeedStyle: Int = 1; var searchFeedStyle: Int = 1;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@ -285,7 +277,6 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class ChannelSettings { class ChannelSettings {
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
} }
@ -311,20 +302,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6) @FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false; var useSubscriptionExchange: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8) @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true; var fetchOnAppBoot: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9) @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true; var fetchOnTabOpen: Boolean = true;
@ -355,16 +342,13 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12) @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false; var showWatchMetrics: Boolean = false;
@AdvancedField
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13) @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true; var allowPlaytimeTracking: Boolean = true;
@AdvancedField
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14) @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false; var alwaysReloadFromCache: Boolean = false;
@AdvancedField
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15) @FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false; var peekChannelContents: Boolean = false;
@ -441,11 +425,9 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5; var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@AdvancedField
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4) @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true; var simplifySources: Boolean = true;
@AdvancedField
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5) @FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@ -456,7 +438,6 @@ class Settings : FragmentedStorageFileJson() {
fun isBackgroundContinue() = backgroundPlay == 1; fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2; fun isBackgroundPictureInPicture() = backgroundPlay == 2;
@AdvancedField
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7) @FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
@DropdownFieldOptionsId(R.array.resume_after_preview) @DropdownFieldOptionsId(R.array.resume_after_preview)
var resumeAfterPreview: Int = 1; var resumeAfterPreview: Int = 1;
@ -483,10 +464,14 @@ class Settings : FragmentedStorageFileJson() {
}; };
} }
@AdvancedField
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9) @FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
var useLiveChatWindow: Boolean = true; var useLiveChatWindow: Boolean = true;
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
var backgroundSwitchToAudio: Boolean = true;
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11) @FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss) @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterLoss: Int = 1; var restartPlaybackAfterLoss: Int = 1;
@ -512,97 +497,8 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21) @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false; var autoplay: Boolean = false;
@AdvancedField
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22) @FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true; var deleteFromWatchLaterAuto: Boolean = true;
@FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23)
@DropdownFieldOptionsId(R.array.seek_offset_duration)
var seekOffset: Int = 2;
fun getSeekOffset(): Long {
return when(seekOffset) {
0 -> 3_000L;
1 -> 5_000L;
2 -> 10_000L;
3 -> 20_000L;
4 -> 30_000L;
5 -> 60_000L;
else -> 10_000L;
}
}
@FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
@DropdownFieldOptionsId(R.array.min_playback_speed)
var minimumPlaybackSpeed: Int = 0;
@FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.max_playback_speed)
var maximumPlaybackSpeed: Int = 2;
@FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.step_playback_speed)
var stepPlaybackSpeed: Int = 1;
fun getPlaybackSpeedStep(): Double {
return when(stepPlaybackSpeed) {
0 -> 0.05
1 -> 0.1
2 -> 0.25
else -> 0.1;
}
}
fun getPlaybackSpeeds(): List<Double> {
val playbackSpeeds = mutableListOf<Double>();
playbackSpeeds.add(1.0);
val minSpeed = when(minimumPlaybackSpeed) {
0 -> 0.25
1 -> 0.5
2 -> 1.0
else -> 0.25
}
val maxSpeed = when(maximumPlaybackSpeed) {
0 -> 2.0
1 -> 2.25
2 -> 3.0
3 -> 4.0
4 -> 5.0
else -> 2.25;
}
var testSpeed = 1.0;
while(testSpeed > minSpeed) {
val nextSpeed = (testSpeed - 0.25) as Double;
testSpeed = Math.max(nextSpeed, minSpeed);
playbackSpeeds.add(testSpeed);
}
testSpeed = 1.0;
while(testSpeed < maxSpeed) {
val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
testSpeed = Math.min(nextSpeed, maxSpeed);
playbackSpeeds.add(testSpeed);
}
playbackSpeeds.sort();
return playbackSpeeds;
}
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
var holdPlaybackSpeed: Int = 4;
fun getHoldPlaybackSpeed(): Double {
return when(holdPlaybackSpeed) {
0 -> 1.0
1 -> 1.25
2 -> 1.5
3 -> 1.75
4 -> 2.0
5 -> 2.25
6 -> 2.5
7 -> 2.75
8 -> 3.0
else -> 2.0
}
}
} }
@FormField(R.string.comments, "group", R.string.comments_description, 6) @FormField(R.string.comments, "group", R.string.comments_description, 6)
@ -618,7 +514,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0) @FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false; var recommendationsDefault: Boolean = false;
@AdvancedField
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0) @FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false; var hideRecommendations: Boolean = false;
@ -655,12 +550,10 @@ class Settings : FragmentedStorageFileJson() {
var preferredAudioQuality: Int = 1; var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0; fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
@AdvancedField
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4) @FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true; var byteRangeDownload: Boolean = true;
@AdvancedField
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5) @FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
@DropdownFieldOptionsId(R.array.thread_count) @DropdownFieldOptionsId(R.array.thread_count)
var byteRangeConcurrency: Int = 3; var byteRangeConcurrency: Int = 3;
@ -690,20 +583,14 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true; var keepScreenOn: Boolean = true;
@AdvancedField
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3) @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false; var alwaysProxyRequests: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4) @FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true; var allowIpv6: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowLinkLocalIpv4: 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)
@ -772,11 +659,9 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Plugins { class Plugins {
@AdvancedField
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1) @FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false; var checkDisabledPluginsForUpdates: Boolean = false;
@AdvancedField
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true; var clearCookiesOnLogout: Boolean = true;
@ -977,23 +862,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1) @FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown"; val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2) @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
fun viewLicenseStatus() {
SettingsActivity.getActivity()?.let {
try {
if (StatePayment.instance.hasPaid) {
val paymentKey = StatePayment.instance.getPaymentKey()
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first)
} else {
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated")
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show license status dialog", e)
}
}
}
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
fun clearPayment() { fun clearPayment() {
SettingsActivity.getActivity()?.let { context -> SettingsActivity.getActivity()?.let { context ->
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", { UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
@ -1011,20 +880,15 @@ class Settings : FragmentedStorageFileJson() {
var other = Other(); var other = Other();
@Serializable @Serializable
class Other { class Other {
@AdvancedField
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true; var playlistDeleteConfirmation: Boolean = true;
@AdvancedField
@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.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4) @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
var watchLaterAddStart: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7) @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
var polycentricLocalCache: Boolean = true; var polycentricLocalCache: Boolean = true;
} }
@ -1062,7 +926,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Synchronization { class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1) @FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = false; var enabled: Boolean = true;
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1) @FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
var broadcast: Boolean = false; var broadcast: Boolean = false;
@ -1072,21 +936,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3) @FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
var connectLast: Boolean = true; var connectLast: Boolean = true;
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
var discoverThroughRelay: Boolean = true;
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
var pairThroughRelay: Boolean = true;
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
var connectThroughRelay: Boolean = true;
@FormField(R.string.connect_local_direct_through_relay, FieldForm.TOGGLE, R.string.connect_local_direct_through_relay_description, 3)
var connectLocalDirectThroughRelay: Boolean = true;
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
var localConnections: Boolean = true;
} }
@FormField(R.string.info, FieldForm.GROUP, -1, 21) @FormField(R.string.info, FieldForm.GROUP, -1, 21)
@ -1155,4 +1004,4 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
//endregion //endregion
} }

View file

@ -319,11 +319,7 @@ class UIDialogs {
closeAction?.invoke() closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE), }, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), { UIDialogs.Action(context.getString(R.string.retry), {
try { retryAction?.invoke();
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
}, UIDialogs.ActionStyle.PRIMARY) }, UIDialogs.ActionStyle.PRIMARY)
); );
else else
@ -337,11 +333,7 @@ class UIDialogs {
closeAction?.invoke() closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE), }, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), { UIDialogs.Action(context.getString(R.string.retry), {
try { retryAction?.invoke();
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
}, UIDialogs.ActionStyle.PRIMARY) }, UIDialogs.ActionStyle.PRIMARY)
); );
} }

View file

@ -4,14 +4,8 @@ import android.app.NotificationManager
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
@ -43,9 +37,6 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.parsers.HLS.MediaRendition
import com.futo.platformplayer.parsers.HLS.StreamInfo
import com.futo.platformplayer.parsers.HLS.VariantPlaylistReference
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StateHistory
@ -72,8 +63,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import androidx.core.net.toUri
class UISlideOverlays { class UISlideOverlays {
companion object { companion object {
@ -310,7 +299,6 @@ class UISlideOverlays {
} }
@OptIn(UnstableApi::class)
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay { fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(LoaderView(container.context)) val items = arrayListOf<View>(LoaderView(container.context))
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items) val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
@ -322,8 +310,6 @@ class UISlideOverlays {
val masterPlaylistContent = masterPlaylistResponse.body?.string() val masterPlaylistContent = masterPlaylistResponse.body?.string()
?: throw Exception("Master playlist content is empty") ?: throw Exception("Master playlist content is empty")
val resolvedPlaylistUrl = masterPlaylistResponse.url
val videoButtons = arrayListOf<SlideUpMenuItem>() val videoButtons = arrayListOf<SlideUpMenuItem>()
val audioButtons = arrayListOf<SlideUpMenuItem>() val audioButtons = arrayListOf<SlideUpMenuItem>()
//TODO: Implement subtitles //TODO: Implement subtitles
@ -336,103 +322,55 @@ class UISlideOverlays {
val masterPlaylist: HLS.MasterPlaylist val masterPlaylist: HLS.MasterPlaylist
try { try {
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray()) masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
.parse(sourceUrl.toUri(), inputStream)
if (playlist is HlsMediaPlaylist) { masterPlaylist.getAudioSources().forEach { it ->
if (source is IHLSManifestAudioSource) {
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
val estSize = VideoHelper.estimateSourceSize(variant); val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem( audioButtons.add(SlideUpMenuItem(
container.context, container.context,
R.drawable.ic_music, R.drawable.ic_music,
variant.name, it.name,
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + variant.codec).trim(), (prefix + it.codec).trim(),
tag = variant, tag = it,
call = { call = {
selectedAudioVariant = variant selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, variant) slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
} else {
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
val estSize = VideoHelper.estimateSourceSize(variant);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
variant.name,
"${variant.width}x${variant.height}",
(prefix + variant.codec).trim(),
tag = variant,
call = {
selectedVideoVariant = variant
slideUpMenuOverlay.selectOption(videoButtons, variant)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
}
} else if (playlist is HlsMultivariantPlaylist) {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
masterPlaylist.getAudioSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
}
/*masterPlaylist.getSubtitleSources().forEach { it ->
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedSubtitleVariant = it
slideUpMenuOverlay.selectOption(subtitleButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download)) slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}, false)) },
}*/ invokeParent = false
))
}
masterPlaylist.getVideoSources().forEach { /*masterPlaylist.getSubtitleSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(it); subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; selectedSubtitleVariant = it
videoButtons.add(SlideUpMenuItem( slideUpMenuOverlay.selectOption(subtitleButtons, it)
container.context, slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
R.drawable.ic_movie, }, false))
it.name, }*/
"${it.width}x${it.height}",
(prefix + it.codec).trim(), masterPlaylist.getVideoSources().forEach {
tag = it, val estSize = VideoHelper.estimateSourceSize(it);
call = { val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
selectedVideoVariant = it videoButtons.add(SlideUpMenuItem(
slideUpMenuOverlay.selectOption(videoButtons, it) container.context,
if (audioButtons.isEmpty()){ R.drawable.ic_movie,
slideUpMenuOverlay.setOk(container.context.getString(R.string.download)) it.name,
} "${it.width}x${it.height}",
}, (prefix + it.codec).trim(),
invokeParent = false tag = it,
)) call = {
} selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
} }
val newItems = arrayListOf<View>() val newItems = arrayListOf<View>()
@ -460,11 +398,11 @@ class UISlideOverlays {
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) { if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (source is IHLSManifestSource) { if (source is IHLSManifestSource) {
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null) StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
UIDialogs.toast(container.context, "Variant video HLS playlist download started") UIDialogs.toast(container.context, "Variant video HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else if (source is IHLSManifestAudioSource) { } else if (source is IHLSManifestAudioSource) {
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null) StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, sourceUrl), null)
UIDialogs.toast(container.context, "Variant audio HLS playlist download started") UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else { } else {
@ -746,10 +684,6 @@ class UISlideOverlays {
} }
} }
} }
if(!Settings.instance.downloads.shouldDownload()) {
UIDialogs.appToast("Download will start when you're back on wifi.\n" +
"(You can change this in settings)", true);
}
} }
}; };
return menu.apply { show() }; return menu.apply { show() };
@ -1046,30 +980,26 @@ class UISlideOverlays {
+ actions).filterNotNull() + actions).filterNotNull()
)); ));
items.add( items.add(
SlideUpMenuGroup( SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
container.context, container.context.getString(R.string.add_to), "addto", SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_queue_add, R.drawable.ic_queue_add,
container.context.getString(R.string.add_to_queue), container.context.getString(R.string.add_to_queue),
"${queue.size} " + container.context.getString(R.string.videos), "${queue.size} " + container.context.getString(R.string.videos),
tag = "queue", tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }), call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem( SlideUpMenuItem(container.context,
container.context,
R.drawable.ic_watchlist_add, R.drawable.ic_watchlist_add,
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
"${watchLater.size} " + container.context.getString(R.string.videos), "${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later", tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }), call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
SlideUpMenuItem( SlideUpMenuItem(container.context,
container.context,
R.drawable.ic_history, R.drawable.ic_history,
container.context.getString(R.string.add_to_history), container.context.getString(R.string.add_to_history),
"Mark as watched", "Mark as watched",
tag = "history", tag = "history",
call = { StateHistory.instance.markAsWatched(video); }), call = { StateHistory.instance.markAsWatched(video); }),
)); ));
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem( playlistItems.add(SlideUpMenuItem(
@ -1133,17 +1063,14 @@ class UISlideOverlays {
val queue = StatePlayer.instance.getQueue(); val queue = StatePlayer.instance.getQueue();
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add( items.add(
SlideUpMenuGroup( SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
container.context, container.context.getString(R.string.other), "other", SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_queue_add, R.drawable.ic_queue_add,
container.context.getString(R.string.queue), container.context.getString(R.string.queue),
"${queue.size} " + container.context.getString(R.string.videos), "${queue.size} " + container.context.getString(R.string.videos),
tag = "queue", tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }), call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem( SlideUpMenuItem(container.context,
container.context,
R.drawable.ic_watchlist_add, R.drawable.ic_watchlist_add,
StatePlayer.TYPE_WATCHLATER, StatePlayer.TYPE_WATCHLATER,
"${watchLater.size} " + container.context.getString(R.string.videos), "${watchLater.size} " + container.context.getString(R.string.videos),
@ -1151,10 +1078,8 @@ class UISlideOverlays {
call = { call = {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false); UIDialogs.appToast("Added to watch later", false);
else
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
}), }),
) )
); );
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
@ -1192,8 +1117,8 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() }; return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
} }
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters { fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues); val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
overlay.show(); overlay.show();
return overlay; return overlay;
} }

View file

@ -28,17 +28,12 @@ import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InterfaceAddress
import java.net.NetworkInterface
import java.net.SocketException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.security.SecureRandom import java.nio.ByteOrder
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.* import java.util.*
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
@ -75,14 +70,7 @@ fun warnIfMainThread(context: String) {
} }
fun ensureNotMainThread() { fun ensureNotMainThread() {
val isMainLooper = try { if (Looper.myLooper() == Looper.getMainLooper()) {
Looper.myLooper() == Looper.getMainLooper()
} catch (e: Throwable) {
//Ignore, for unit tests where its not mocked
false
}
if (isMainLooper) {
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread") Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
throw IllegalStateException("Cannot run on main thread") throw IllegalStateException("Cannot run on main thread")
} }
@ -285,7 +273,7 @@ fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
} }
} }
if(newIndex < 0) if(newIndex < 0)
return newArr.size; return originalArr.size;
else else
return newIndex; return newIndex;
} }
@ -296,18 +284,6 @@ fun ByteBuffer.toUtf8String(): String {
return String(remainingBytes, Charsets.UTF_8) return String(remainingBytes, Charsets.UTF_8)
} }
fun generateReadablePassword(length: Int): String {
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
val secureRandom = SecureRandom()
val randomBytes = ByteArray(length)
secureRandom.nextBytes(randomBytes)
val sb = StringBuilder(length)
for (byte in randomBytes) {
val index = (byte.toInt() and 0xFF) % validChars.length
sb.append(validChars[index])
}
return sb.toString()
}
fun ByteArray.toGzip(): ByteArray { fun ByteArray.toGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0) if (this == null || this.isEmpty()) return ByteArray(0)
@ -337,125 +313,4 @@ fun ByteArray.fromGzip(): ByteArray {
} }
} }
return outputStream.toByteArray() return outputStream.toByteArray()
} }
fun findCandidateAddresses(): List<InetAddress> {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.sortedWith(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
).map { it.second.address }
}
fun findPreferredAddress(): InetAddress? {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.minWithOrNull(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
)?.second?.address
}
private fun isUsableInterface(nif: NetworkInterface): Boolean {
val name = nif.name.lowercase()
return try {
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
nif.isUp
&& !nif.isLoopback
&& !nif.isPointToPoint
&& !nif.isVirtual
&& !name.startsWith("docker")
&& !name.startsWith("veth")
&& !name.startsWith("br-")
&& !name.startsWith("virbr")
&& !name.startsWith("vmnet")
&& !name.startsWith("tun")
&& !name.startsWith("tap")
} catch (e: SocketException) {
false
}
}
private fun isUsableAddress(addr: InetAddress): Boolean {
return when {
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
addr.isLoopbackAddress -> false
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
addr.isMulticastAddress -> false
else -> true
}
}
private fun interfaceScore(nif: NetworkInterface): Int {
val name = nif.name.lowercase()
return when {
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
name.startsWith("eth") || name.contains("ethernet") -> 0
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
name.contains("wi-fi") || name.contains("wifi") -> 1
else -> 2
}
}
fun addressScore(addr: InetAddress): Int {
return when (addr) {
is Inet4Address -> {
val octets = addr.address.map { it.toInt() and 0xFF }
when {
octets[0] == 10 -> 0 // 10/8
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.1631/12
else -> 1 // public IPv4
}
}
is Inet6Address -> {
// ULA (fc00::/7) vs global vs others
val b0 = addr.address[0].toInt() and 0xFF
when {
(b0 and 0xFE) == 0xFC -> 2 // ULA
(b0 and 0xE0) == 0x20 -> 3 // global
else -> 4
}
}
else -> Int.MAX_VALUE
}
}
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)

View file

@ -1,15 +1,14 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.content.ComponentName
import android.app.UiModeManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.VmPolicy import android.os.StrictMode.VmPolicy
@ -22,7 +21,6 @@ import android.widget.ImageView
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
@ -32,8 +30,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStateAtLeast
import androidx.lifecycle.withStateAtLeast
import androidx.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
@ -42,9 +38,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
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.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
@ -71,9 +65,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
@ -82,6 +74,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.receivers.MediaButtonReceiver
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.StateBackup import com.futo.platformplayer.states.StateBackup
@ -115,7 +108,6 @@ 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.Queue
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -154,8 +146,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//Frags Main //Frags Main
lateinit var _fragMainHome: HomeFragment; lateinit var _fragMainHome: HomeFragment;
lateinit var _fragPostDetail: PostDetailFragment; lateinit var _fragPostDetail: PostDetailFragment;
lateinit var _fragArticleDetail: ArticleDetailFragment;
lateinit var _fragWebDetail: WebDetailFragment;
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment; lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment; lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment; lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
@ -185,7 +175,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragVideoDetail: VideoDetailFragment; lateinit var _fragVideoDetail: VideoDetailFragment;
//State //State
private val _queue: LinkedList<Pair<MainFragment, Any?>> = LinkedList(); private val _queue: Queue<Pair<MainFragment, Any?>> = LinkedList();
lateinit var fragCurrent: MainFragment private set; lateinit var fragCurrent: MainFragment private set;
private var _parameterCurrent: Any? = null; private var _parameterCurrent: Any? = null;
@ -195,9 +185,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private var _isVisible = true; private var _isVisible = true;
private var _wasStopped = false; private var _wasStopped = false;
private var _privateModeEnabled = false
private var _pictureInPictureEnabled = false
private var _isFullscreen = false
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)
@ -209,7 +196,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
try { try {
lifecycleScope.launch { runBlocking {
handleUrlAll(content) handleUrlAll(content)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@ -219,8 +206,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
val mainId = UUID.randomUUID().toString().substring(0, 5)
constructor() : super() { constructor() : super() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy( StrictMode.setVmPolicy(
@ -272,15 +257,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
@UnstableApi @UnstableApi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Logger.w(TAG, "MainActivity Starting [$mainId]"); Logger.i(TAG, "MainActivity Starting");
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId); StateApp.instance.setGlobalContext(this, lifecycleScope);
StateApp.instance.mainAppStarting(this); StateApp.instance.mainAppStarting(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val uiMode = getSystemService(UiModeManager::class.java)
uiMode.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
}
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
if (Settings.instance.playback.allowVideoToGoUnderCutout) if (Settings.instance.playback.allowVideoToGoUnderCutout)
@ -288,11 +269,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
runBlocking { runBlocking {
try { StatePlatform.instance.updateAvailableClients(this@MainActivity);
StatePlatform.instance.updateAvailableClients(this@MainActivity);
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in updateAvailableClients", e)
}
} }
//Preload common files to memory //Preload common files to memory
@ -336,8 +313,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainPlaylist = PlaylistFragment.newInstance(); _fragMainPlaylist = PlaylistFragment.newInstance();
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance(); _fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
_fragPostDetail = PostDetailFragment.newInstance(); _fragPostDetail = PostDetailFragment.newInstance();
_fragArticleDetail = ArticleDetailFragment.newInstance();
_fragWebDetail = WebDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance(); _fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance(); _fragHistory = HistoryFragment.newInstance();
_fragSourceDetail = SourceDetailFragment.newInstance(); _fragSourceDetail = SourceDetailFragment.newInstance();
@ -379,18 +354,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSubscriptionsFeed.setPreviewsEnabled(true); _fragMainSubscriptionsFeed.setPreviewsEnabled(true);
_fragContainerVideoDetail.visibility = View.INVISIBLE; _fragContainerVideoDetail.visibility = View.INVISIBLE;
updateSegmentPaddings(); updateSegmentPaddings();
updatePrivateModeVisibility()
}; };
_buttonIncognito = findViewById(R.id.incognito_button); _buttonIncognito = findViewById(R.id.incognito_button);
updatePrivateModeVisibility() _buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
StateApp.instance.privateModeChanged.subscribe { StateApp.instance.privateModeChanged.subscribe {
//Messing with visibility causes some issues with layout ordering? //Messing with visibility causes some issues with layout ordering?
_privateModeEnabled = it if (it) {
updatePrivateModeVisibility() _buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
} }
_buttonIncognito.setOnClickListener { _buttonIncognito.setOnClickListener {
if (!StateApp.instance.privateMode) if (!StateApp.instance.privateMode)
return@setOnClickListener; return@setOnClickListener;
@ -407,16 +386,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}; };
_fragVideoDetail.onFullscreenChanged.subscribe { _fragVideoDetail.onFullscreenChanged.subscribe {
Logger.i(TAG, "onFullscreenChanged ${it}"); Logger.i(TAG, "onFullscreenChanged ${it}");
_isFullscreen = it
updatePrivateModeVisibility()
}
_fragVideoDetail.onMinimize.subscribe { if (it) {
updatePrivateModeVisibility() _buttonIncognito.elevation = -99f;
} _buttonIncognito.alpha = 0f;
} else {
_fragVideoDetail.onMaximized.subscribe { if (StateApp.instance.privateMode) {
updatePrivateModeVisibility() _buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
} }
StatePlayer.instance.also { StatePlayer.instance.also {
@ -464,8 +446,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainPlaylist.topBar = _fragTopBarNavigation; _fragMainPlaylist.topBar = _fragTopBarNavigation;
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation; _fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
_fragPostDetail.topBar = _fragTopBarNavigation; _fragPostDetail.topBar = _fragTopBarNavigation;
_fragArticleDetail.topBar = _fragTopBarNavigation;
_fragWebDetail.topBar = _fragTopBarNavigation;
_fragWatchlist.topBar = _fragTopBarNavigation; _fragWatchlist.topBar = _fragTopBarNavigation;
_fragHistory.topBar = _fragTopBarNavigation; _fragHistory.topBar = _fragTopBarNavigation;
_fragSourceDetail.topBar = _fragTopBarNavigation; _fragSourceDetail.topBar = _fragTopBarNavigation;
@ -633,18 +613,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work"); UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
}*/ }*/
private var _qrCodeLoadingDialog: AlertDialog? = null
fun showUrlQrCodeScanner() { fun showUrlQrCodeScanner() {
try { try {
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
"Launching QR scanner",
"Make sure your camera is enabled", null, -2,
UIDialogs.Action("Close", {
_qrCodeLoadingDialog?.dismiss()
_qrCodeLoadingDialog = null
}));
val integrator = IntentIntegrator(this) val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code)) integrator.setPrompt(getString(R.string.scan_a_qr_code))
@ -660,36 +630,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
@OptIn(UnstableApi::class)
private fun updatePrivateModeVisibility() {
if (_privateModeEnabled && (_fragVideoDetail.state == State.CLOSED || !_pictureInPictureEnabled && !_isFullscreen)) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
_buttonIncognito.translationY = if (_fragVideoDetail.state == State.MINIMIZED) -60.dp(resources).toFloat() else 0f
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
override fun onResume() { override fun onResume() {
super.onResume(); super.onResume();
Logger.w(TAG, "onResume [$mainId]") Logger.v(TAG, "onResume")
_isVisible = true; _isVisible = true;
} }
override fun onPause() { override fun onPause() {
super.onPause(); super.onPause();
Logger.w(TAG, "onPause [$mainId]") Logger.v(TAG, "onPause")
_isVisible = false; _isVisible = false;
_qrCodeLoadingDialog?.dismiss()
_qrCodeLoadingDialog = null
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
Logger.w(TAG, "onStop [$mainId]"); Logger.v(TAG, "_wasStopped = true");
_wasStopped = true; _wasStopped = true;
} }
@ -723,7 +678,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"VIDEO" -> { "VIDEO" -> {
val url = intent.getStringExtra("VIDEO"); val url = intent.getStringExtra("VIDEO");
navigateWhenReady(_fragVideoDetail, url); navigate(_fragVideoDetail, url);
} }
"IMPORT_OPTIONS" -> { "IMPORT_OPTIONS" -> {
@ -741,11 +696,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"Sources" -> { "Sources" -> {
runBlocking { runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed.. StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
navigateWhenReady(_fragMainSources); navigate(_fragMainSources);
} }
}; };
"BROWSE_PLUGINS" -> { "BROWSE_PLUGINS" -> {
navigateWhenReady(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf( navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
Pair("grayjay") { req -> Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let { StateApp.instance.contextOrNull?.let {
if (it is MainActivity) { if (it is MainActivity) {
@ -763,12 +718,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
try { try {
if (targetData != null) { if (targetData != null) {
lifecycleScope.launch(Dispatchers.Main) { runBlocking {
try { handleUrlAll(targetData)
handleUrlAll(targetData)
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
}
} }
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -796,10 +747,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
startActivity(intent); startActivity(intent);
} else if (url.startsWith("grayjay://video/")) { } else if (url.startsWith("grayjay://video/")) {
val videoUrl = url.substring("grayjay://video/".length); val videoUrl = url.substring("grayjay://video/".length);
navigateWhenReady(_fragVideoDetail, videoUrl); navigate(_fragVideoDetail, videoUrl);
} else if (url.startsWith("grayjay://channel/")) { } else if (url.startsWith("grayjay://channel/")) {
val channelUrl = url.substring("grayjay://channel/".length); val channelUrl = url.substring("grayjay://channel/".length);
navigateWhenReady(_fragMainChannel, channelUrl); navigate(_fragMainChannel, channelUrl);
} }
} }
@ -865,29 +816,29 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
Logger.i(TAG, "handleUrl(url=$url) on IO"); Logger.i(TAG, "handleUrl(url=$url) on IO");
if (StatePlatform.instance.hasEnabledContentClient(url)) { if (StatePlatform.instance.hasEnabledVideoClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found video client"); Logger.i(TAG, "handleUrl(url=$url) found video client");
withContext(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
if (position > 0) if (position > 0)
navigateWhenReady(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true)); navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
else else
navigateWhenReady(_fragVideoDetail, url); navigate(_fragVideoDetail, url);
_fragVideoDetail.maximizeVideoDetail(true); _fragVideoDetail.maximizeVideoDetail(true);
} }
return@withContext true; return@withContext true;
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) { } else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found channel client"); Logger.i(TAG, "handleUrl(url=$url) found channel client");
withContext(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
navigateWhenReady(_fragMainChannel, url); navigate(_fragMainChannel, url);
delay(100); delay(100);
_fragVideoDetail.minimizeVideoDetail(); _fragVideoDetail.minimizeVideoDetail();
}; };
return@withContext true; return@withContext true;
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) { } else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found playlist client"); Logger.i(TAG, "handleUrl(url=$url) found playlist client");
withContext(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
navigateWhenReady(_fragMainRemotePlaylist, url); navigate(_fragMainRemotePlaylist, url);
delay(100); delay(100);
_fragVideoDetail.minimizeVideoDetail(); _fragVideoDetail.minimizeVideoDetail();
}; };
@ -1099,33 +1050,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop") Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig); _fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
Logger.v(TAG, "onPictureInPictureModeChanged Ready"); Logger.v(TAG, "onPictureInPictureModeChanged Ready");
_pictureInPictureEnabled = isInPictureInPictureMode
updatePrivateModeVisibility()
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy(); super.onDestroy();
Logger.w(TAG, "onDestroy [$mainId]") Logger.v(TAG, "onDestroy")
StateApp.instance.mainAppDestroyed(this, mainId); StateApp.instance.mainAppDestroyed(this);
} }
inline fun <reified T> isFragmentActive(): Boolean { inline fun <reified T> isFragmentActive(): Boolean {
return fragCurrent is T; return fragCurrent is T;
} }
fun navigateWhenReady(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
navigate(segment, parameter, withHistory, isBack)
} else {
lifecycleScope.launch {
lifecycle.withStateAtLeast(Lifecycle.State.RESUMED) {
navigate(segment, parameter, withHistory, isBack)
}
}
}
}
/** /**
* Navigate takes a MainFragment, and makes them the current main visible view * Navigate takes a MainFragment, and makes them the current main visible view
* A parameter can be provided which becomes available in the onShow of said fragment * A parameter can be provided which becomes available in the onShow of said fragment
@ -1187,6 +1123,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory) if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
fragBeforeOverlay = fragCurrent; fragBeforeOverlay = fragCurrent;
fragCurrent = segment; fragCurrent = segment;
_parameterCurrent = parameter; _parameterCurrent = parameter;
} }
@ -1249,8 +1186,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
PlaylistFragment::class -> _fragMainPlaylist as T; PlaylistFragment::class -> _fragMainPlaylist as T;
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T; RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
PostDetailFragment::class -> _fragPostDetail as T; PostDetailFragment::class -> _fragPostDetail as T;
ArticleDetailFragment::class -> _fragArticleDetail 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;
SourceDetailFragment::class -> _fragSourceDetail as T; SourceDetailFragment::class -> _fragSourceDetail as T;

View file

@ -14,12 +14,10 @@ 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
@ -31,9 +29,6 @@ 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
@ -44,7 +39,6 @@ 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))
@ -55,47 +49,24 @@ 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();
}; };
_imageQR.visibility = View.INVISIBLE _exportBundle = createExportBundle();
_textQR.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
lifecycleScope.launch { try {
try { val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
val pair = withContext(Dispatchers.IO) { val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
val bundle = createExportBundle() _imageQR.setImageBitmap(qrCodeBitmap);
val dimension = TypedValue.applyDimension( } catch (e: Exception) {
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
).toInt() _imageQR.visibility = View.INVISIBLE;
val qr = generateQRCode(bundle, dimension, dimension) _textQR.visibility = View.INVISIBLE;
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 {

View file

@ -9,8 +9,6 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
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.StateSync import com.futo.platformplayer.states.StateSync
@ -31,16 +29,6 @@ class SyncHomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (StateApp.instance.contextOrNull == null) {
Logger.w(TAG, "No main activity, restarting main.")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
finish()
return
}
setContentView(R.layout.activity_sync_home) setContentView(R.layout.activity_sync_home)
setNavigationBarColorAndIcons() setNavigationBarColorAndIcons()
@ -66,6 +54,7 @@ class SyncHomeActivity : AppCompatActivity() {
val view = _viewMap[publicKey] val view = _viewMap[publicKey]
if (!session.isAuthorized) { if (!session.isAuthorized) {
if (view != null) { if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(publicKey) _viewMap.remove(publicKey)
} }
return@launch return@launch
@ -100,20 +89,6 @@ class SyncHomeActivity : AppCompatActivity() {
updateEmptyVisibility() updateEmptyVisibility()
} }
} }
StateSync.instance.confirmStarted(this, onStarted = {
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
}
if (StateSync.instance.syncService?.relayConnected == false) {
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
}
if (StateSync.instance.syncService?.serverSocketStarted == false) {
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
}
}, onNotStarted = {
finish()
})
} }
override fun onDestroy() { override fun onDestroy() {
@ -125,12 +100,10 @@ class SyncHomeActivity : AppCompatActivity() {
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView { private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false val connected = session?.connected ?: false
val authorized = session?.isAuthorized ?: false syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey) .setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key? //TODO: also display public key?
.setStatus(if (connected && authorized) "Connected" else "Disconnected or unauthorized") .setStatus(if (connected) "Connected" else "Disconnected")
return syncDeviceView return syncDeviceView
} }

View file

@ -83,7 +83,6 @@ class SyncPairActivity : AppCompatActivity() {
_layoutPairingSuccess.setOnClickListener { _layoutPairingSuccess.setOnClickListener {
_layoutPairingSuccess.visibility = View.GONE _layoutPairingSuccess.visibility = View.GONE
finish()
} }
_layoutPairingError.setOnClickListener { _layoutPairingError.setOnClickListener {
_layoutPairingError.visibility = View.GONE _layoutPairingError.visibility = View.GONE
@ -110,17 +109,11 @@ class SyncPairActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
StateSync.instance.syncService?.connect(deviceInfo) { complete, message -> StateSync.instance.connect(deviceInfo) { session, complete, message ->
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
if (complete != null) { if (complete) {
if (complete) { _layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairingSuccess.visibility = View.VISIBLE _layoutPairing.visibility = View.GONE
_layoutPairing.visibility = View.GONE
} else {
_textError.text = message
_layoutPairingError.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
}
} else { } else {
_textPairingStatus.text = message _textPairingStatus.text = message
} }
@ -144,6 +137,8 @@ class SyncPairActivity : AppCompatActivity() {
_textError.text = e.message _textError.text = e.message
_layoutPairing.visibility = View.GONE _layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e) Logger.e(TAG, "Failed to pair", e)
} finally {
_layoutPairing.visibility = View.GONE
} }
} }

View file

@ -67,18 +67,11 @@ class SyncShowPairingCodeActivity : AppCompatActivity() {
} }
val ips = getIPs() val ips = getIPs()
val publicKey = StateSync.instance.syncService?.publicKey val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT)
val pairingCode = StateSync.instance.syncService?.pairingCode val json = Json.encodeToString(selfDeviceInfo)
if (publicKey == null || pairingCode == null) { val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
setCode("Public key or pairing code was not known, is sync enabled?") val url = "grayjay://sync/${base64}"
} else { setCode(url)
val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, pairingCode)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
}
} }
fun setCode(code: String?) { fun setCode(code: String?) {

View file

@ -90,7 +90,6 @@ open class ManagedHttpClient {
} }
fun tryHead(url: String): Map<String, String>? { fun tryHead(url: String): Map<String, String>? {
ensureNotMainThread()
try { try {
val result = head(url); val result = head(url);
if(result.isOk) if(result.isOk)
@ -105,7 +104,7 @@ open class ManagedHttpClient {
} }
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket { fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
ensureNotMainThread()
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.url(url); .url(url);
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" }) if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
@ -301,7 +300,6 @@ open class ManagedHttpClient {
} }
fun send(msg: String) { fun send(msg: String) {
ensureNotMainThread()
socket.send(msg); socket.send(msg);
} }

View file

@ -1,6 +1,5 @@
package com.futo.platformplayer.api.media package com.futo.platformplayer.api.media
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -67,11 +66,6 @@ interface IPlatformClient {
*/ */
fun searchChannels(query: String): IPager<PlatformAuthorLink>; fun searchChannels(query: String): IPager<PlatformAuthorLink>;
/**
* Searches for channels and returns a content pager
*/
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
//Video Pages //Video Pages
/** /**

View file

@ -14,16 +14,14 @@ class PlatformClientPool {
private var _poolCounter = 0; private var _poolCounter = 0;
private val _poolName: String?; private val _poolName: String?;
private val _privatePool: Boolean; private val _privatePool: Boolean;
private val _isolatedInitialization: Boolean
var isDead: Boolean = false var isDead: Boolean = false
private set; private set;
val onDead = Event2<JSClient, PlatformClientPool>(); val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false, isolatedInitialization: Boolean = false) { constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
_poolName = name; _poolName = name;
_privatePool = privatePool; _privatePool = privatePool;
_isolatedInitialization = isolatedInitialization
if(parentClient !is JSClient) if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now"); throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started"); Logger.i(TAG, "Pool for ${parentClient.name} was started");
@ -34,10 +32,8 @@ class PlatformClientPool {
isDead = true; isDead = true;
onDead.emit(parentClient, this); onDead.emit(parentClient, this);
synchronized(_pool) { for(clientPair in _pool) {
for (clientPair in _pool) { clientPair.key.disable();
clientPair.key.disable();
}
} }
}; };
} }
@ -57,7 +53,7 @@ class PlatformClientPool {
reserved = _pool.keys.find { !it.isBusy }; reserved = _pool.keys.find { !it.isBusy };
if(reserved == null && _pool.size < capacity) { if(reserved == null && _pool.size < capacity) {
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})"); Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
reserved = _parent.getCopy(_privatePool, _isolatedInitialization); reserved = _parent.getCopy(_privatePool);
reserved?.onCaptchaException?.subscribe { client, ex -> reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex); StateApp.instance.handleCaptchaException(client, ex);

View file

@ -2,7 +2,6 @@ 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
@ -45,7 +44,6 @@ 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),

View file

@ -7,15 +7,13 @@ class PlatformMultiClientPool {
private var _isFake = false; private var _isFake = false;
private var _privatePool = false; private var _privatePool = false;
private val _isolatedInitialization: Boolean
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false, isolatedInitialization: Boolean = false) { constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
_name = name; _name = name;
_maxCap = if(maxCap > 0) _maxCap = if(maxCap > 0)
maxCap maxCap
else 99; else 99;
_privatePool = isPrivatePool; _privatePool = isPrivatePool;
_isolatedInitialization = isolatedInitialization
} }
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient { fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
@ -23,7 +21,7 @@ class PlatformMultiClientPool {
return parentClient; return parentClient;
val pool = synchronized(_clientPools) { val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient)) if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool, _isolatedInitialization).apply { _clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
this.onDead.subscribe { _, pool -> this.onDead.subscribe { _, pool ->
synchronized(_clientPools) { synchronized(_clientPools) {
if(_clientPools[parentClient] == pool) if(_clientPools[parentClient] == pool)

View file

@ -2,11 +2,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.models.contents.ContentType
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.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -34,7 +30,6 @@ 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);
@ -47,21 +42,4 @@ open class PlatformAuthorLink {
); );
} }
} }
}
interface IPlatformChannelContent : IPlatformContent {
val thumbnail: String?
val subscribers: Long?
}
open class JSChannelContent : JSContent, IPlatformChannelContent {
override val contentType: ContentType get() = ContentType.CHANNEL
override val thumbnail: String?
override val subscribers: Long?
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Channel";
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
}
} }

View file

@ -3,7 +3,6 @@ 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
@ -21,7 +20,6 @@ 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),

View file

@ -5,7 +5,6 @@ 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
@ -47,7 +46,6 @@ 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"); },
@ -71,7 +69,6 @@ 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)
@ -93,7 +90,6 @@ 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"),

View file

@ -4,7 +4,6 @@ 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
@ -32,7 +31,6 @@ 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) }

View file

@ -1,9 +0,0 @@
package com.futo.platformplayer.api.media.models.article
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
interface IPlatformArticle: IPlatformContent {
val summary: String?;
val thumbnails: Thumbnails?;
}

View file

@ -1,12 +0,0 @@
package com.futo.platformplayer.api.media.models.article
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.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
val segments: List<IJSArticleSegment>;
val rating : IRating;
}

View file

@ -8,12 +8,10 @@ enum class ContentType(val value: Int) {
POST(2), POST(2),
ARTICLE(3), ARTICLE(3),
PLAYLIST(4), PLAYLIST(4),
WEB(7),
URL(9), URL(9),
NESTED_VIDEO(11), NESTED_VIDEO(11),
CHANNEL(60),
LOCKED(70), LOCKED(70),

View file

@ -2,7 +2,6 @@ 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 {
@ -11,7 +10,6 @@ interface IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
obj.ensureIsBusy();
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) { return when(t) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj); LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);

View file

@ -4,7 +4,6 @@ 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
@ -28,8 +27,6 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
obj.ensureIsBusy();
val contextName = "LiveEventComment" val contextName = "LiveEventComment"
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null); val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);

View file

@ -3,7 +3,6 @@ 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
@ -38,7 +37,6 @@ 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),

View file

@ -2,7 +2,6 @@ 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 {
@ -16,9 +15,9 @@ class LiveEventEmojis: IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
obj.ensureIsBusy();
val contextName = "LiveEventEmojis" val contextName = "LiveEventEmojis"
return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName)); return LiveEventEmojis(
obj.getOrThrow(config, "emojis", contextName));
} }
} }
} }

View file

@ -2,8 +2,6 @@ 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 {
@ -12,24 +10,20 @@ 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, isOutgoing: Boolean) { constructor(name: String, url: String, thumbnail: String) {
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);
} }
} }
} }

View file

@ -2,7 +2,6 @@ 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 {
@ -16,7 +15,6 @@ class LiveEventViewCount: IPlatformLiveEvent {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
obj.ensureIsBusy();
val contextName = "LiveEventViewCount" val contextName = "LiveEventViewCount"
return LiveEventViewCount( return LiveEventViewCount(
obj.getOrThrow(config, "viewCount", contextName)); obj.getOrThrow(config, "viewCount", contextName));

View file

@ -5,8 +5,7 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
enum class TextType(val value: Int) { enum class TextType(val value: Int) {
RAW(0), RAW(0),
HTML(1), HTML(1),
MARKUP(2), MARKUP(2);
CODE(3);
companion object { companion object {
fun fromInt(value: Int): TextType fun fromInt(value: Int): TextType

View file

@ -3,7 +3,6 @@ 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
@ -14,12 +13,8 @@ interface IRating {
companion object { companion object {
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating { fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
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);

View file

@ -2,7 +2,6 @@ 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
/** /**
@ -15,7 +14,6 @@ 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"));
} }
} }

View file

@ -2,7 +2,6 @@ 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,7 +13,6 @@ 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"));
} }
} }

View file

@ -2,7 +2,6 @@ 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,7 +13,6 @@ 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"));
} }
} }

View file

@ -54,12 +54,8 @@ class DevJSClient : JSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings); return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
} }
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient { override fun getCopy(privateCopy: Boolean): JSClient {
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID); return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
} }
override fun initialize() { override fun initialize() {

View file

@ -10,7 +10,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -32,7 +31,6 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
import com.futo.platformplayer.api.media.platforms.js.models.JSComment import com.futo.platformplayer.api.media.platforms.js.models.JSComment
@ -59,13 +57,9 @@ 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
@ -87,8 +81,6 @@ 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;
@ -104,14 +96,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() = _plugin.isBusy; val isBusy: Boolean get() = _busyCounter > 0;
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>;
@ -201,12 +193,8 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit); _plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
} }
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient { open fun getCopy(withoutCredentials: Boolean = false): JSClient {
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials); return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
} }
fun getUnderlyingPlugin(): V8Plugin { fun getUnderlyingPlugin(): V8Plugin {
@ -220,31 +208,12 @@ 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 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;
@ -284,28 +253,19 @@ 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() = isBusyWith("enable") { fun 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? = isBusyWith("saveState") { fun saveState(): String? {
ensureEnabled(); ensureEnabled();
if(!capabilities.hasSaveState) if(!capabilities.hasSaveState)
return@isBusyWith null; return null;
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value; val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
return@isBusyWith resp; return resp;
} }
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped") @JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
@ -346,10 +306,8 @@ open class JSClient : IPlatformClient {
return _searchCapabilities!!; return _searchCapabilities!!;
} }
return busy { _searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()")); return _searchCapabilities!!;
return@busy _searchCapabilities!!;
}
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getSearchCapabilities", ex); announcePluginUnhandledException("getSearchCapabilities", ex);
@ -377,10 +335,8 @@ open class JSClient : IPlatformClient {
if (_searchChannelContentsCapabilities != null) if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!; return _searchChannelContentsCapabilities!!;
return busy { _searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()")); return _searchChannelContentsCapabilities!!;
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")
@ -405,21 +361,17 @@ open class JSClient : IPlatformClient {
return@isBusyWith JSChannelPager(config, this, return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})")); plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
} }
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
ensureEnabled();
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
}
@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 = isBusyWith("isChannelUrl") { override fun isChannelUrl(url: String): Boolean {
try { try {
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})") return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
.value; .value;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex); announcePluginUnhandledException("isChannelUrl", ex);
return@isBusyWith false; return false;
} }
} }
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url") @JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@ -437,10 +389,9 @@ 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@busy _channelCapabilities!!; return _channelCapabilities!!;
};
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getChannelCapabilities", ex); announcePluginUnhandledException("getChannelCapabilities", ex);
@ -551,14 +502,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 = isBusyWith("isContentDetailsUrl") { override fun isContentDetailsUrl(url: String): Boolean {
try { try {
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})") return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
.value; .value;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isContentDetailsUrl", ex); announcePluginUnhandledException("isContentDetailsUrl", ex);
return@isBusyWith false; return false;
} }
} }
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url") @JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@ -590,7 +541,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(this, tracker); return@isBusyWith JSPlaybackTracker(config, tracker);
else else
return@isBusyWith null; return@isBusyWith null;
} }
@ -632,6 +583,7 @@ 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") {
@ -659,19 +611,17 @@ 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 = isBusyWith("isPlaylistUrl") { override fun isPlaylistUrl(url: String): Boolean {
if (!capabilities.hasGetPlaylist) if (!capabilities.hasGetPlaylist)
return@isBusyWith false; return false;
try { try {
return@isBusyWith busy { return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})") .value;
.value;
}
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex); announcePluginUnhandledException("isPlaylistUrl", ex);
return@isBusyWith false; return false;
} }
} }
@JSOptional @JSOptional
@ -773,29 +723,19 @@ open class JSClient : IPlatformClient {
return urls; return urls;
} }
fun <T> busy(handle: ()->T): T {
return _plugin.busy {
return@busy handle();
}
}
fun <T> busyBlockingSuspended(handle: suspend ()->T): T {
return _plugin.busy {
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();
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
try {
synchronized(_busyLock) {
_busyCounter++;
} }
finally { _busyAction = actionName;
_busyAction = ""; return handle();
}
finally {
_busyAction = "";
synchronized(_busyLock) {
_busyCounter--;
} }
} }
} }

View file

@ -4,7 +4,6 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
@ -169,17 +168,12 @@ class SourcePluginConfig(
} }
fun validate(text: String): Boolean { fun validate(text: String): Boolean {
try { if(scriptPublicKey.isNullOrEmpty())
if (scriptPublicKey.isNullOrEmpty()) throw IllegalStateException("No public key present");
throw IllegalStateException("No public key present"); if(scriptSignature.isNullOrEmpty())
if (scriptSignature.isNullOrEmpty()) throw IllegalStateException("No signature present");
throw IllegalStateException("No signature present");
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey); return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
return false
}
} }
fun isUrlAllowed(url: String): Boolean { fun isUrlAllowed(url: String): Boolean {
@ -210,8 +204,6 @@ class SourcePluginConfig(
obj.sourceUrl = sourceUrl; obj.sourceUrl = sourceUrl;
return obj; return obj;
} }
private val TAG = "SourcePluginConfig"
} }
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable

View file

@ -67,25 +67,6 @@ 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)) })
@ -146,7 +127,7 @@ class JSHttpClient : ManagedHttpClient {
} }
if(doApplyCookies) { if(doApplyCookies) {
if (_currentCookieMap.isNotEmpty() || _otherCookieMap.isNotEmpty()) { if (_currentCookieMap.isNotEmpty()) {
val cookiesToApply = hashMapOf<String, String>(); val cookiesToApply = hashMapOf<String, String>();
synchronized(_currentCookieMap) { synchronized(_currentCookieMap) {
for(cookie in _currentCookieMap for(cookie in _currentCookieMap
@ -154,12 +135,6 @@ class JSHttpClient : ManagedHttpClient {
.flatMap { it.value.toList() }) .flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second; cookiesToApply[cookie.first] = cookie.second;
}; };
synchronized(_otherCookieMap) {
for(cookie in _otherCookieMap
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
}
if(cookiesToApply.size > 0) { if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; "); val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");

View file

@ -1,12 +1,10 @@
package com.futo.platformplayer.api.media.platforms.js.models 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.JSChannelContent
import com.futo.platformplayer.api.media.models.contents.ContentType 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
@ -14,7 +12,6 @@ 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);
@ -29,9 +26,6 @@ interface IJSContent: IPlatformContent {
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj); ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
ContentType.PLAYLIST -> JSPlaylist(config, obj); ContentType.PLAYLIST -> JSPlaylist(config, obj);
ContentType.LOCKED -> JSLockedContent(config, obj); ContentType.LOCKED -> JSLockedContent(config, obj);
ContentType.CHANNEL -> JSChannelContent(config, obj);
ContentType.ARTICLE -> JSArticle(config, obj);
ContentType.WEB -> JSWeb(config, obj);
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }

View file

@ -6,20 +6,17 @@ 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);
ContentType.POST -> JSPostDetails(plugin.config, obj); ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj); ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
ContentType.WEB -> JSWebDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }

View file

@ -1,39 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
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.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
final override val contentType: ContentType get() = ContentType.ARTICLE;
override val summary: String;
override val thumbnails: Thumbnails?;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformArticle";
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
}
}

View file

@ -4,8 +4,6 @@ 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.IPluginSourced import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
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.ContentType 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
@ -23,20 +21,20 @@ import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails { open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.ARTICLE; final override val contentType: ContentType get() = ContentType.ARTICLE;
private val _hasGetComments: Boolean; private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean; private val _hasGetContentRecommendations: Boolean;
override val rating: IRating; val rating: IRating;
override val summary: String; val summary: String;
override val thumbnails: Thumbnails?; val thumbnails: Thumbnails?;
override val segments: List<IJSArticleSegment>; val segments: List<IJSArticleSegment>;
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) { constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformArticle"; val contextName = "PlatformPost";
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0); rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
summary = _content.getOrThrow(client.config, "summary", contextName); summary = _content.getOrThrow(client.config, "summary", contextName);
@ -101,7 +99,6 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) { return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj); SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj); SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.HEADER -> JSHeaderSegment(client, obj);
SegmentType.NESTED -> JSNestedSegment(client, obj); SegmentType.NESTED -> JSNestedSegment(client, obj);
else -> null; else -> null;
} }
@ -113,7 +110,6 @@ enum class SegmentType(val value: Int) {
UNKNOWN(0), UNKNOWN(0),
TEXT(1), TEXT(1),
IMAGES(2), IMAGES(2),
HEADER(3),
NESTED(9); NESTED(9);
@ -154,17 +150,6 @@ class JSImagesSegment: IJSArticleSegment {
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: ""; caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
} }
} }
class JSHeaderSegment: IJSArticleSegment {
override val type = SegmentType.HEADER;
val content: String;
val level: Int;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSHeaderSegment";
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
level = obj.getOrDefault(client.config, "level", contextName, 1) ?: 1;
}
}
class JSNestedSegment: IJSArticleSegment { class JSNestedSegment: IJSArticleSegment {
override val type = SegmentType.NESTED; override val type = SegmentType.NESTED;
val nested: IPlatformContent; val nested: IPlatformContent;

View file

@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> { class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {

View file

@ -49,8 +49,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
else else
author = PlatformAuthorLink.UNKNOWN; author = PlatformAuthorLink.UNKNOWN;
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong(); val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
if(datetimeInt == null || datetimeInt == 0.toLong()) if(datetimeInt == 0.toLong())
datetime = null; datetime = null;
else else
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC); datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);

View file

@ -2,7 +2,6 @@ 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.IPluginSourced import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.JSChannelContent
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
@ -16,14 +15,4 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
override fun convertResult(obj: V8ValueObject): IPlatformContent { override fun convertResult(obj: V8ValueObject): IPlatformContent {
return IJSContent.fromV8(plugin, obj); return IJSContent.fromV8(plugin, obj);
} }
}
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
override val sourceConfig: SourcePluginConfig get() = config;
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformContent {
return JSChannelContent(config, obj);
}
} }

View file

@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }
override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") { override fun nextPage() {
super.nextPage(); super.nextPage();
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }

View file

@ -29,9 +29,7 @@ abstract class JSPager<T> : IPager<T> {
this.pager = pager; this.pager = pager;
this.config = config; this.config = config;
plugin.busy { _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}
getResults(); getResults();
} }
@ -46,14 +44,11 @@ abstract class JSPager<T> : IPager<T> {
override fun nextPage() { override fun nextPage() {
warnIfMainThread("JSPager.nextPage"); warnIfMainThread("JSPager.nextPage");
val pluginV8 = plugin.getUnderlyingPlugin(); pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pluginV8.busy { pager.invoke("nextPage", arrayOf<Any>());
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { };
pager.invoke("nextPage", arrayOf<Any>()); _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}; _resultChanged = true;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
}
/* /*
try { try {
} }
@ -75,18 +70,15 @@ abstract class JSPager<T> : IPager<T> {
return previousResults; return previousResults;
warnIfMainThread("JSPager.getResults"); warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
return plugin.getUnderlyingPlugin().busy { if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager"); throw IllegalStateException("Runtime closed");
if (items.v8Runtime.isDead || items.v8Runtime.isClosed) val newResults = items.toArray()
throw IllegalStateException("Runtime closed"); .map { convertResult(it as V8ValueObject) }
val newResults = items.toArray() .toList();
.map { convertResult(it as V8ValueObject) } _lastResults = newResults;
.toList(); _resultChanged = false;
_lastResults = newResults; return newResults;
_resultChanged = false;
return@busy newResults;
}
} }
abstract fun convertResult(obj: V8ValueObject): T; abstract fun convertResult(obj: V8ValueObject): T;

View file

@ -2,50 +2,37 @@ 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.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 lateinit var _client: JSClient; private val _config: IV8PluginConfig;
private lateinit var _config: IV8PluginConfig; private val _obj: V8ValueObject;
private lateinit var _obj: V8ValueObject;
private var _hasCalledInit: Boolean = false; private var _hasCalledInit: Boolean = false;
private var _hasInit: Boolean = false; private val _hasInit: Boolean;
private var _lastRequest: Long = Long.MIN_VALUE; private var _lastRequest: Long = Long.MIN_VALUE;
private var _hasOnConcluded: Boolean = false; private val _hasOnConcluded: Boolean;
override var nextRequest: Int = 1000 override var nextRequest: Int = 1000
private set; private set;
constructor(client: JSClient, obj: V8ValueObject) { constructor(config: IV8PluginConfig, 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");
client.busy { this._config = config;
if (!obj.has("onProgress")) this._obj = obj;
throw ScriptImplementationException( this._hasInit = obj.has("onInit");
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) {
@ -53,15 +40,12 @@ class JSPlaybackTracker: IPlaybackTracker {
synchronized(_obj) { synchronized(_obj) {
if(_hasCalledInit) if(_hasCalledInit)
return; return;
if (_hasInit) {
_client.busy { Logger.i("JSPlaybackTracker", "onInit (${seconds})");
if (_hasInit) { _obj.invokeVoid("onInit", seconds);
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeVoid("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;
} }
} }
@ -71,12 +55,10 @@ class JSPlaybackTracker: IPlaybackTracker {
if(!_hasCalledInit && _hasInit) if(!_hasCalledInit && _hasInit)
onInit(seconds); onInit(seconds);
else { else {
_client.busy { Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying);
_obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); _lastRequest = System.currentTimeMillis();
_lastRequest = System.currentTimeMillis();
}
} }
} }
} }
@ -85,9 +67,7 @@ class JSPlaybackTracker: IPlaybackTracker {
if(_hasOnConcluded) { if(_hasOnConcluded) {
synchronized(_obj) { synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded"); Logger.i("JSPlaybackTracker", "onConcluded");
_client.busy { _obj.invokeVoid("onConcluded", -1);
_obj.invokeVoid("onConcluded", -1);
}
} }
} }
} }

View file

@ -46,18 +46,16 @@ class JSRequestExecutor {
if (_executor.isClosed) if (_executor.isClosed)
throw IllegalStateException("Executor object is closed"); throw IllegalStateException("Executor object is closed");
return _plugin.getUnderlyingPlugin().busy { val result = if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
val result = if(_plugin is DevJSClient) V8Plugin.catchScriptErrors<Any>(
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { _config,
V8Plugin.catchScriptErrors<Any>( "[${_config.name}] JSRequestExecutor",
_config, "builder.modifyRequest()"
"[${_config.name}] JSRequestExecutor", ) {
"builder.modifyRequest()" _executor.invoke("executeRequest", url, headers, method, body);
) { } as V8Value;
_executor.invoke("executeRequest", url, headers, method, body); }
} as V8Value;
}
else V8Plugin.catchScriptErrors<Any>( else V8Plugin.catchScriptErrors<Any>(
_config, _config,
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
@ -66,35 +64,34 @@ class JSRequestExecutor {
_executor.invoke("executeRequest", url, headers, method, body); _executor.invoke("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@busy base64Result; return 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@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);
} }
finally { if(result is V8ValueTypedArray) {
result.close(); 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 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);
}
finally {
result.close();
} }
} }
@ -102,25 +99,24 @@ 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.invokeVoid("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.invokeVoid("cleanup", null);
}; };
}
} }
protected fun finalize() { protected fun finalize() {

View file

@ -16,7 +16,7 @@ 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 = false; override var allowByteSkip: Boolean;
constructor(plugin: JSClient, modifier: V8ValueObject) { constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin; this._plugin = plugin;
@ -24,13 +24,10 @@ class JSRequestModifier: IRequestModifier {
this._config = plugin.config; this._config = plugin.config;
val config = plugin.config; val config = plugin.config;
plugin.busy { allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
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 {
@ -38,15 +35,13 @@ class JSRequestModifier: IRequestModifier {
return Request(url, headers); return Request(url, headers);
} }
return _plugin.busy { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { _modifier.invoke("modifyRequest", url, headers);
_modifier.invoke("modifyRequest", url, headers); } as V8ValueObject;
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers); val req = JSRequest(_plugin, result, url, headers);
result.close(); result.close();
return@busy req; return req;
}
} }

View file

@ -6,7 +6,6 @@ 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.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -36,11 +35,8 @@ 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 _obj.getSourcePlugin()?.busy { return v8String.value;
val v8String = _obj.invoke<V8ValueString>("getSubtitles", arrayOf<Any>());
return@busy v8String.value;
} ?: "";
} }
override suspend fun getSubtitlesURI(): Uri? { override suspend fun getSubtitlesURI(): Uri? {

View file

@ -27,7 +27,6 @@ import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
class JSVideoDetails : JSVideo, IPlatformVideoDetails { class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private val _plugin: JSClient;
private val _hasGetComments: Boolean; private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean; private val _hasGetContentRecommendations: Boolean;
private val _hasGetPlaybackTracker: Boolean; private val _hasGetPlaybackTracker: Boolean;
@ -49,7 +48,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
val contextName = "VideoDetails"; val contextName = "VideoDetails";
_plugin = plugin;
val config = plugin.config; val config = plugin.config;
description = _content.getOrThrow(config, "description", contextName); description = _content.getOrThrow(config, "description", contextName);
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
@ -84,16 +82,14 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return getPlaybackTrackerJS(); return getPlaybackTrackerJS();
} }
private fun getPlaybackTrackerJS(): IPlaybackTracker? { private fun getPlaybackTrackerJS(): IPlaybackTracker? {
return _plugin.busy { return V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") {
V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") { val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>())
val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>()) ?: return@catchScriptErrors null;
?: return@catchScriptErrors null; if(tracker is V8ValueObject)
if(tracker is V8ValueObject) return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker);
return@catchScriptErrors JSPlaybackTracker(_plugin, tracker); else
else return@catchScriptErrors null;
return@catchScriptErrors null; };
}
}
} }
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? { override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
@ -110,10 +106,8 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return null; return null;
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
return _plugin.busy { val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); return JSContentPager(_pluginConfig, client, contentPager);
return@busy JSContentPager(_pluginConfig, client, contentPager);
}
} }
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? { override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
@ -129,12 +123,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
} }
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? { private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
return _plugin.busy { val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>()); if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? return null;
return@busy null;
return@busy JSCommentPager(_pluginConfig, client, commentPager); return JSCommentPager(_pluginConfig, client, commentPager);
}
} }
} }

View file

@ -1,31 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
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.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSWeb : JSContent, IPluginSourced {
final override val contentType: ContentType get() = ContentType.WEB;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformWeb";
}
}

View file

@ -1,41 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
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.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSWebDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.WEB;
val html: String?;
//TODO: Options?
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformWeb";
html = obj.getOrDefault(client.config, "html", contextName, null);
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
}

View file

@ -1,8 +1,6 @@
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
@ -15,13 +13,8 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.others.Language import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource { class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String; override val container : String;
@ -57,44 +50,6 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
hasGenerate = _obj.has("generate"); hasGenerate = _obj.has("generate");
} }
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = null;
if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
}
return plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
return@busy result.convert {
it.value
};
}
}
override fun generate(): String? { override fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
@ -107,27 +62,21 @@ 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()") {
_plugin.isBusyWith("dashAudio.generate") { _obj.invokeString("generate");
_obj.invokeV8<V8ValueString>("generate").value;
}
} }
} }
else else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") { _obj.invokeString("generate");
_obj.invokeV8<V8ValueString>("generate").value;
}
} }
if(result != null){ if(result != null){
plugin.busy { val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
} }
} }
return result; return result;

View file

@ -3,7 +3,6 @@ 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
@ -16,18 +15,11 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
interface IJSDashManifestRawSource { interface IJSDashManifestRawSource {
val hasGenerate: Boolean; val hasGenerate: Boolean;
var manifest: String?; var manifest: String?;
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
fun generate(): String?; fun generate(): String?;
} }
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource { open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
@ -40,7 +32,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
override val duration: Long; override val duration: Long;
override val priority: Boolean; override val priority: Boolean;
val url: String?; var url: String?;
override var manifest: String?; override var manifest: String?;
override val hasGenerate: Boolean; override val hasGenerate: Boolean;
@ -65,45 +57,6 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
hasGenerate = _obj.has("generate"); hasGenerate = _obj.has("generate");
} }
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = null;
if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
});
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
});
return plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
return@busy result.convert {
it.value
};
}
}
override open fun generate(): String? { override open fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
@ -114,28 +67,22 @@ 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()", {
_plugin.isBusyWith("dashVideo.generate") { _obj.invokeString("generate");
_obj.invokeV8<V8ValueString>("generate").value;
}
}); });
} }
} }
else else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") { _obj.invokeString("generate");
_obj.invokeV8<V8ValueString>("generate").value;
}
}); });
if(result != null){ if(result != null){
_plugin.busy { val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
} }
} }
return result; return result;
@ -163,32 +110,6 @@ 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();

View file

@ -7,7 +7,6 @@ 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
@ -39,13 +38,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
companion object { companion object {
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? { fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
obj?.ensureIsBusy(); fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj);
return obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }
};
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource {
obj.ensureIsBusy();
return JSHLSManifestAudioSource(plugin, obj)
};
} }
} }

View file

@ -14,7 +14,6 @@ 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.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
@ -54,39 +53,36 @@ abstract class JSSource {
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor"); hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
} }
fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") { fun getRequestModifier(): IRequestModifier? {
if(_requestModifier != null) if(_requestModifier != null)
return@isBusyWith AdhocRequestModifier { url, headers -> return 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@isBusyWith null; return 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.invoke("getRequestModifier", arrayOf<Any>());
}; };
if (result !is V8ValueObject) if (result !is V8ValueObject)
return@isBusyWith null; return null;
return@isBusyWith JSRequestModifier(_plugin, result) return JSRequestModifier(_plugin, result)
} }
open fun getRequestExecutor(): JSRequestExecutor? = _plugin.isBusyWith("getRequestExecutor") { open fun getRequestExecutor(): JSRequestExecutor? {
if (!hasRequestExecutor || _obj.isClosed) if (!hasRequestExecutor || _obj.isClosed)
return@isBusyWith null; return 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.invoke("getRequestExecutor", arrayOf<Any>());
}; };
Logger.v("JSSource", "Request executor for [${type}] received");
if (result !is V8ValueObject) if (result !is V8ValueObject)
return@isBusyWith null; return null;
return@isBusyWith JSRequestExecutor(_plugin, result) return JSRequestExecutor(_plugin, result)
} }
fun getUnderlyingPlugin(): JSClient? { fun getUnderlyingPlugin(): JSClient? {
@ -109,12 +105,8 @@ 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? { fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
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);
@ -131,26 +123,13 @@ 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{ fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
obj.ensureIsBusy(); fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj);
return JSDashManifestSource(plugin, obj) fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(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 { fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
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);

View file

@ -6,7 +6,6 @@ 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 {
@ -32,7 +31,6 @@ 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);

View file

@ -149,7 +149,6 @@ class AirPlayCastingDevice : CastingDevice {
break; break;
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to get setup initial connection to AirPlay device.", e) Logger.w(TAG, "Failed to get setup initial connection to AirPlay device.", e)
delay(1000);
} }
} }

View file

@ -108,7 +108,7 @@ abstract class CastingDevice {
val expectedCurrentTime: Double val expectedCurrentTime: Double
get() { get() {
val diff = if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0; val diff = (System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0;
return time + diff; return time + diff;
}; };
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED

View file

@ -10,9 +10,7 @@ import com.futo.platformplayer.toHexString
import com.futo.platformplayer.toInetAddress import com.futo.platformplayer.toInetAddress
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
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 org.json.JSONObject import org.json.JSONObject
@ -35,7 +33,7 @@ class ChromecastCastingDevice : CastingDevice {
override var usedRemoteAddress: InetAddress? = null; override var usedRemoteAddress: InetAddress? = null;
override var localAddress: InetAddress? = null; override var localAddress: InetAddress? = null;
override val canSetVolume: Boolean get() = true; override val canSetVolume: Boolean get() = true;
override val canSetSpeed: Boolean get() = true; override val canSetSpeed: Boolean get() = false; //TODO: Implement
var addresses: Array<InetAddress>? = null; var addresses: Array<InetAddress>? = null;
var port: Int = 0; var port: Int = 0;
@ -58,10 +56,6 @@ class ChromecastCastingDevice : CastingDevice {
private var _mediaSessionId: Int? = null; private var _mediaSessionId: Int? = null;
private var _thread: Thread? = null; private var _thread: Thread? = null;
private var _pingThread: Thread? = null; private var _pingThread: Thread? = null;
private var _launchRetries = 0
private val MAX_LAUNCH_RETRIES = 3
private var _lastLaunchTime_ms = 0L
private var _retryJob: Job? = null
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() { constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
this.name = name; this.name = name;
@ -144,23 +138,6 @@ class ChromecastCastingDevice : CastingDevice {
sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json); sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json);
} }
override fun changeSpeed(speed: Double) {
if (invokeInIOScopeIfRequired { changeSpeed(speed) }) return
val speedClamped = speed.coerceAtLeast(1.0).coerceAtLeast(1.0).coerceAtMost(2.0)
setSpeed(speedClamped)
val mediaSessionId = _mediaSessionId ?: return
val transportId = _transportId ?: return
val setSpeedObject = JSONObject().apply {
put("type", "SET_PLAYBACK_RATE")
put("mediaSessionId", mediaSessionId)
put("playbackRate", speedClamped)
put("requestId", _requestId++)
}
sendChannelMessage(sourceId = "sender-0", destinationId = transportId, namespace = "urn:x-cast:com.google.cast.media", json = setSpeedObject.toString())
}
override fun changeVolume(volume: Double) { override fun changeVolume(volume: Double) {
if (invokeInIOScopeIfRequired({ changeVolume(volume) })) { if (invokeInIOScopeIfRequired({ changeVolume(volume) })) {
return; return;
@ -252,7 +229,6 @@ class ChromecastCastingDevice : CastingDevice {
launchObject.put("appId", "CC1AD845"); launchObject.put("appId", "CC1AD845");
launchObject.put("requestId", _requestId++); launchObject.put("requestId", _requestId++);
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString()); sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString());
_lastLaunchTime_ms = System.currentTimeMillis()
} }
private fun getStatus() { private fun getStatus() {
@ -292,7 +268,6 @@ class ChromecastCastingDevice : CastingDevice {
_contentType = null; _contentType = null;
_streamType = null; _streamType = null;
_sessionId = null; _sessionId = null;
_launchRetries = 0
_transportId = null; _transportId = null;
} }
@ -307,7 +282,6 @@ class ChromecastCastingDevice : CastingDevice {
_started = true; _started = true;
_sessionId = null; _sessionId = null;
_launchRetries = 0
_mediaSessionId = null; _mediaSessionId = null;
Logger.i(TAG, "Starting..."); Logger.i(TAG, "Starting...");
@ -348,7 +322,6 @@ class ChromecastCastingDevice : CastingDevice {
break; break;
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e) Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
Thread.sleep(1000);
} }
} }
@ -361,10 +334,6 @@ class ChromecastCastingDevice : CastingDevice {
//Connection loop //Connection loop
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
_sessionId = null;
_launchRetries = 0
_mediaSessionId = null;
Logger.i(TAG, "Connecting to Chromecast."); Logger.i(TAG, "Connecting to Chromecast.");
connectionState = CastConnectionState.CONNECTING; connectionState = CastConnectionState.CONNECTING;
@ -423,7 +392,7 @@ class ChromecastCastingDevice : CastingDevice {
try { try {
val inputStream = _inputStream ?: break; val inputStream = _inputStream ?: break;
val message = synchronized(_inputStreamLock) synchronized(_inputStreamLock)
{ {
Log.d(TAG, "Receiving next packet..."); Log.d(TAG, "Receiving next packet...");
val b1 = inputStream.readUnsignedByte(); val b1 = inputStream.readUnsignedByte();
@ -435,7 +404,7 @@ class ChromecastCastingDevice : CastingDevice {
if (size > buffer.size) { if (size > buffer.size) {
Logger.w(TAG, "Skipping packet that is too large $size bytes.") Logger.w(TAG, "Skipping packet that is too large $size bytes.")
inputStream.skip(size.toLong()); inputStream.skip(size.toLong());
return@synchronized null return@synchronized
} }
Log.d(TAG, "Received header indicating $size bytes. Waiting for message."); Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
@ -444,19 +413,15 @@ class ChromecastCastingDevice : CastingDevice {
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end? //TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
val messageBytes = buffer.sliceArray(IntRange(0, size - 1)); val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}."); Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
val msg = ChromeCast.CastMessage.parseFrom(messageBytes); val message = ChromeCast.CastMessage.parseFrom(messageBytes);
if (msg.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") { if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
Logger.i(TAG, "Received message: $msg"); Logger.i(TAG, "Received message: $message");
} }
return@synchronized msg
}
if (message != null) {
try { try {
handleMessage(message); handleMessage(message);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to handle message.", e); Logger.w(TAG, "Failed to handle message.", e);
break
} }
} }
} catch (e: java.net.SocketException) { } catch (e: java.net.SocketException) {
@ -520,10 +485,6 @@ class ChromecastCastingDevice : CastingDevice {
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to send channel message (sourceId: $sourceId, destinationId: $destinationId, namespace: $namespace, json: $json)", e); Logger.w(TAG, "Failed to send channel message (sourceId: $sourceId, destinationId: $destinationId, namespace: $namespace, json: $json)", e);
_socket?.close();
Logger.i(TAG, "Socket disconnected.");
connectionState = CastConnectionState.CONNECTING;
} }
} }
@ -550,7 +511,6 @@ class ChromecastCastingDevice : CastingDevice {
if (_sessionId == null) { if (_sessionId == null) {
connectionState = CastConnectionState.CONNECTED; connectionState = CastConnectionState.CONNECTED;
_sessionId = applicationUpdate.getString("sessionId"); _sessionId = applicationUpdate.getString("sessionId");
_launchRetries = 0
val transportId = applicationUpdate.getString("transportId"); val transportId = applicationUpdate.getString("transportId");
connectMediaChannel(transportId); connectMediaChannel(transportId);
@ -565,40 +525,21 @@ class ChromecastCastingDevice : CastingDevice {
} }
if (!sessionIsRunning) { if (!sessionIsRunning) {
if (System.currentTimeMillis() - _lastLaunchTime_ms > 5000) { _sessionId = null;
_sessionId = null _mediaSessionId = null;
_mediaSessionId = null setTime(0.0);
setTime(0.0) _transportId = null;
_transportId = null Logger.w(TAG, "Session not found.");
if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) { if (_launching) {
Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}") Logger.i(TAG, "Player not found, launching.");
_launchRetries++ launchPlayer();
launchPlayer()
} else if (!_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
// Maybe the first GET_STATUS came back empty; still try launching
Logger.i(TAG, "Player not found; triggering launch #${_launchRetries + 1}")
_launching = true
_launchRetries++
launchPlayer()
} else {
Logger.e(TAG, "Player not found after $_launchRetries attempts; giving up.")
Logger.i(TAG, "Unable to start media receiver on device")
stop()
}
} else { } else {
if (_retryJob == null) { Logger.i(TAG, "Player not found, disconnecting.");
Logger.i(TAG, "Scheduled retry job over 5 seconds") stop();
_retryJob = _scopeIO?.launch(Dispatchers.IO) {
delay(5000)
getStatus()
_retryJob = null
}
}
} }
} else { } else {
_launching = false _launching = false;
_launchRetries = 0
} }
val volume = status.getJSONObject("volume"); val volume = status.getJSONObject("volume");
@ -625,7 +566,7 @@ class ChromecastCastingDevice : CastingDevice {
} }
isPlaying = playerState == "PLAYING"; isPlaying = playerState == "PLAYING";
if (isPlaying || playerState == "PAUSED") { if (isPlaying) {
setTime(currentTime); setTime(currentTime);
} }
@ -640,8 +581,6 @@ class ChromecastCastingDevice : CastingDevice {
if (message.sourceId == "receiver-0") { if (message.sourceId == "receiver-0") {
Logger.i(TAG, "Close received."); Logger.i(TAG, "Close received.");
stop(); stop();
} else if (_transportId == message.sourceId) {
throw Exception("Transport id closed.")
} }
} }
} else { } else {
@ -676,9 +615,6 @@ class ChromecastCastingDevice : CastingDevice {
localAddress = null; localAddress = null;
_started = false; _started = false;
_retryJob?.cancel()
_retryJob = null
val socket = _socket; val socket = _socket;
val scopeIO = _scopeIO; val scopeIO = _scopeIO;

View file

@ -25,7 +25,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
@ -93,7 +92,7 @@ class FCastCastingDevice : CastingDevice {
private var _version: Long = 1; private var _version: Long = 1;
private var _thread: Thread? = null private var _thread: Thread? = null
private var _pingThread: Thread? = null private var _pingThread: Thread? = null
@Volatile private var _lastPongTime = System.currentTimeMillis() private var _lastPongTime = -1L
private var _outputStreamLock = Object() private var _outputStreamLock = Object()
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() { constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
@ -290,7 +289,6 @@ class FCastCastingDevice : CastingDevice {
break; break;
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e) Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e)
Thread.sleep(1000);
} }
} }
@ -328,9 +326,9 @@ class FCastCastingDevice : CastingDevice {
continue; continue;
} }
localAddress = _socket?.localAddress localAddress = _socket?.localAddress;
_lastPongTime = System.currentTimeMillis() connectionState = CastConnectionState.CONNECTED;
connectionState = CastConnectionState.CONNECTED _lastPongTime = -1L
val buffer = ByteArray(4096); val buffer = ByteArray(4096);
@ -406,32 +404,36 @@ class FCastCastingDevice : CastingDevice {
_pingThread = Thread { _pingThread = Thread {
Logger.i(TAG, "Started ping loop.") Logger.i(TAG, "Started ping loop.")
while (_scopeIO?.isActive == true) { while (_scopeIO?.isActive == true) {
if (connectionState == CastConnectionState.CONNECTED) { try {
send(Opcode.Ping)
} catch (e: Throwable) {
Log.w(TAG, "Failed to send ping.")
try { try {
send(Opcode.Ping) _socket?.close()
if (System.currentTimeMillis() - _lastPongTime > 15000) { _inputStream?.close()
Logger.w(TAG, "Closing socket due to last pong time being larger than 15 seconds.") _outputStream?.close()
try {
_socket?.close()
} catch (e: Throwable) {
Log.w(TAG, "Failed to close socket.", e)
}
}
} catch (e: Throwable) { } catch (e: Throwable) {
Log.w(TAG, "Failed to send ping.") Log.w(TAG, "Failed to close socket.", e)
try {
_socket?.close()
_inputStream?.close()
_outputStream?.close()
} catch (e: Throwable) {
Log.w(TAG, "Failed to close socket.", e)
}
} }
} }
Thread.sleep(5000)
/*if (_lastPongTime != -1L && System.currentTimeMillis() - _lastPongTime > 6000) {
Logger.w(TAG, "Closing socket due to last pong time being larger than 6 seconds.")
try {
_socket?.close()
} catch (e: Throwable) {
Log.w(TAG, "Failed to close socket.", e)
}
}*/
Thread.sleep(2000)
} }
Logger.i(TAG, "Stopped ping loop.")
Logger.i(TAG, "Stopped ping loop.");
}.apply { start() } }.apply { start() }
} else { } else {
Log.i(TAG, "Thread was still alive, not restarted") Log.i(TAG, "Thread was still alive, not restarted")

View file

@ -4,14 +4,10 @@ import android.app.AlertDialog
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
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 java.net.NetworkInterface import android.util.Xml
import java.net.Inet4Address
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.R import com.futo.platformplayer.R
@ -43,8 +39,9 @@ import com.futo.platformplayer.builders.DashBuilder
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.findPreferredAddress
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.mdns.DnsService
import com.futo.platformplayer.mdns.ServiceDiscoverer
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
@ -58,11 +55,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.net.Inet6Address import java.io.ByteArrayInputStream
import java.net.InetAddress import java.net.InetAddress
import java.net.URLDecoder import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Collections
import java.util.UUID import java.util.UUID
class StateCasting { class StateCasting {
@ -74,6 +70,7 @@ class StateCasting {
private var _started = false; private var _started = false;
var devices: HashMap<String, CastingDevice> = hashMapOf(); var devices: HashMap<String, CastingDevice> = hashMapOf();
var rememberedDevices: ArrayList<CastingDevice> = arrayListOf();
val onDeviceAdded = Event1<CastingDevice>(); val onDeviceAdded = Event1<CastingDevice>();
val onDeviceChanged = Event1<CastingDevice>(); val onDeviceChanged = Event1<CastingDevice>();
val onDeviceRemoved = Event1<CastingDevice>(); val onDeviceRemoved = Event1<CastingDevice>();
@ -87,15 +84,48 @@ class StateCasting {
private var _audioExecutor: JSRequestExecutor? = null private var _audioExecutor: JSRequestExecutor? = null
private val _client = ManagedHttpClient(); private val _client = ManagedHttpClient();
var _resumeCastingDevice: CastingDeviceInfo? = null; var _resumeCastingDevice: CastingDeviceInfo? = null;
private var _nsdManager: NsdManager? = null val _serviceDiscoverer = ServiceDiscoverer(arrayOf(
"_googlecast._tcp.local",
"_airplay._tcp.local",
"_fastcast._tcp.local",
"_fcast._tcp.local"
)) { handleServiceUpdated(it) }
val isCasting: Boolean get() = activeDevice != null; val isCasting: Boolean get() = activeDevice != null;
private val _discoveryListeners = mapOf( private fun handleServiceUpdated(services: List<DnsService>) {
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice), for (s in services) {
"_airplay._tcp" to createDiscoveryListener(::addOrUpdateAirPlayDevice), //TODO: Addresses IPv4 only?
"_fastcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice), val addresses = s.addresses.toTypedArray()
"_fcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice) val port = s.port.toInt()
) var name = s.texts.firstOrNull { it.startsWith("md=") }?.substring("md=".length)
if (s.name.endsWith("._googlecast._tcp.local")) {
if (name == null) {
name = s.name.substring(0, s.name.length - "._googlecast._tcp.local".length)
}
addOrUpdateChromeCastDevice(name, addresses, port)
} else if (s.name.endsWith("._airplay._tcp.local")) {
if (name == null) {
name = s.name.substring(0, s.name.length - "._airplay._tcp.local".length)
}
addOrUpdateAirPlayDevice(name, addresses, port)
} else if (s.name.endsWith("._fastcast._tcp.local")) {
if (name == null) {
name = s.name.substring(0, s.name.length - "._fastcast._tcp.local".length)
}
addOrUpdateFastCastDevice(name, addresses, port)
} else if (s.name.endsWith("._fcast._tcp.local")) {
if (name == null) {
name = s.name.substring(0, s.name.length - "._fcast._tcp.local".length)
}
addOrUpdateFastCastDevice(name, addresses, port)
}
}
}
fun handleUrl(context: Context, url: String) { fun handleUrl(context: Context, url: String) {
val uri = Uri.parse(url) val uri = Uri.parse(url)
@ -160,34 +190,30 @@ class StateCasting {
Logger.i(TAG, "CastingService starting..."); Logger.i(TAG, "CastingService starting...");
rememberedDevices.clear();
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
_castServer.start(); _castServer.start();
enableDeveloper(true); enableDeveloper(true);
Logger.i(TAG, "CastingService started."); Logger.i(TAG, "CastingService started.");
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
startDiscovering()
} }
@Synchronized @Synchronized
private fun startDiscovering() { fun startDiscovering() {
_nsdManager?.apply { try {
_discoveryListeners.forEach { _serviceDiscoverer.start()
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value) } catch (e: Throwable) {
} Logger.i(TAG, "Failed to start ServiceDiscoverer", e)
} }
} }
@Synchronized @Synchronized
private fun stopDiscovering() { fun stopDiscovering() {
_nsdManager?.apply { try {
_discoveryListeners.forEach { _serviceDiscoverer.stop()
try { } catch (e: Throwable) {
stopServiceDiscovery(it.value) Logger.i(TAG, "Failed to stop ServiceDiscoverer", e)
} catch (e: Throwable) {
Logger.w(TAG, "Failed to stop service discovery", e)
}
}
} }
} }
@ -213,85 +239,6 @@ class StateCasting {
_castServer.removeAllHandlers(); _castServer.removeAllHandlers();
Logger.i(TAG, "CastingService stopped.") 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)
}
})
}
}
}
} }
private val _castingDialogLock = Any(); private val _castingDialogLock = Any();
@ -347,9 +294,7 @@ class StateCasting {
UIDialogs.toast(it, "Connecting to device...") UIDialogs.toast(it, "Connecting to device...")
synchronized(_castingDialogLock) { synchronized(_castingDialogLock) {
if(_currentDialog == null) { if(_currentDialog == null) {
_currentDialog = UIDialogs.showDialog(context, R.drawable.ic_loader_animated, true, _currentDialog = UIDialogs.showDialog(context, R.drawable.ic_loader_animated, true, "Connecting to [${device.name}]", "Make sure you are on the same network", null, -2,
"Connecting to [${device.name}]",
"Make sure you are on the same network\n\nVPNs and guest networks can cause issues", null, -2,
UIDialogs.Action("Disconnect", { UIDialogs.Action("Disconnect", {
device.stop(); device.stop();
})); }));
@ -384,6 +329,9 @@ class StateCasting {
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) }; invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
}; };
addRememberedDevice(device);
Logger.i(TAG, "Device added to active discovery. Active discovery now contains ${_storage.getDevicesCount()} devices.")
try { try {
device.start(); device.start();
} catch (e: Throwable) { } catch (e: Throwable) {
@ -405,22 +353,21 @@ class StateCasting {
return addRememberedDevice(device); return addRememberedDevice(device);
} }
fun getRememberedCastingDevices(): List<CastingDevice> {
return _storage.getDevices().map { deviceFromCastingDeviceInfo(it) }
}
fun getRememberedCastingDeviceNames(): List<String> {
return _storage.getDeviceNames()
}
fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo { fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo {
val deviceInfo = device.getDeviceInfo() val deviceInfo = device.getDeviceInfo()
return _storage.addDevice(deviceInfo) val foundInfo = _storage.addDevice(deviceInfo)
if (foundInfo == deviceInfo) {
rememberedDevices.add(device);
return foundInfo;
}
return foundInfo;
} }
fun removeRememberedDevice(device: CastingDevice) { fun removeRememberedDevice(device: CastingDevice) {
val name = device.name ?: return val name = device.name ?: return;
_storage.removeDevice(name) _storage.removeDevice(name);
rememberedDevices.remove(device);
} }
private fun invokeInMainScopeIfRequired(action: () -> Unit){ private fun invokeInMainScopeIfRequired(action: () -> Unit){
@ -489,7 +436,7 @@ class StateCasting {
} }
} else { } else {
val proxyStreams = Settings.instance.casting.alwaysProxyRequests; val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
if (videoSource is IVideoUrlSource) { if (videoSource is IVideoUrlSource) {
@ -584,7 +531,7 @@ class StateCasting {
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> { private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val videoPath = "/video-${id}" val videoPath = "/video-${id}"
val videoUrl = url + videoPath; val videoUrl = url + videoPath;
@ -603,7 +550,7 @@ class StateCasting {
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> { private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val audioPath = "/audio-${id}" val audioPath = "/audio-${id}"
val audioUrl = url + audioPath; val audioUrl = url + audioPath;
@ -622,7 +569,7 @@ class StateCasting {
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> { private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
val ad = activeDevice ?: return listOf() val ad = activeDevice ?: return listOf()
val url = getLocalUrl(ad) val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
val id = UUID.randomUUID() val id = UUID.randomUUID()
val hlsPath = "/hls-${id}" val hlsPath = "/hls-${id}"
@ -718,7 +665,7 @@ class StateCasting {
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> { private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val dashPath = "/dash-${id}" val dashPath = "/dash-${id}"
@ -768,7 +715,7 @@ class StateCasting {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice; val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val videoPath = "/video-${id}" val videoPath = "/video-${id}"
@ -833,7 +780,7 @@ class StateCasting {
_castServer.removeAllHandlers("castProxiedHlsMaster") _castServer.removeAllHandlers("castProxiedHlsMaster")
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val hlsPath = "/hls-${id}" val hlsPath = "/hls-${id}"
@ -1003,7 +950,7 @@ class StateCasting {
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> { private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val hlsPath = "/hls-${id}" val hlsPath = "/hls-${id}"
@ -1133,7 +1080,7 @@ class StateCasting {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice; val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val dashPath = "/dash-${id}" val dashPath = "/dash-${id}"
@ -1219,22 +1166,6 @@ class StateCasting {
} }
} }
private fun getLocalUrl(ad: CastingDevice): String {
var address = ad.localAddress!!
if (Settings.instance.casting.allowLinkLocalIpv4) {
if (address.isLinkLocalAddress && address is Inet6Address) {
address = findPreferredAddress() ?: address
Logger.i(TAG, "Selected casting address: $address")
}
} else {
if (address.isLinkLocalAddress) {
address = findPreferredAddress() ?: address
Logger.i(TAG, "Selected casting address: $address")
}
}
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> { private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();
@ -1242,7 +1173,7 @@ class StateCasting {
cleanExecutors() cleanExecutors()
_castServer.removeAllHandlers("castDashRaw") _castServer.removeAllHandlers("castDashRaw")
val url = getLocalUrl(ad); val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
val id = UUID.randomUUID(); val id = UUID.randomUUID();
val dashPath = "/dash-${id}" val dashPath = "/dash-${id}"

View file

@ -82,11 +82,7 @@ class TaskHandler<TParameter, TResult> {
handled = true; handled = true;
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e); Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e);
try { onError.emit(e, parameter);
onError.emit(e, parameter);
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in .exception handler 1", e)
}
handled = true; handled = true;
} }
} }
@ -103,14 +99,10 @@ class TaskHandler<TParameter, TResult> {
if (id != _idGenerator) if (id != _idGenerator)
return@withContext; return@withContext;
try { if (!onError.emit(e, parameter)) {
if (!onError.emit(e, parameter)) { Logger.e(TAG, "Uncaught exception handled by TaskHandler.", e);
Logger.e(TAG, "Uncaught exception handled by TaskHandler.", e); } else {
} else { //Logger.w(TAG, "Handled exception in TaskHandler invoke.", e); (Prevents duplicate logs)
//Logger.w(TAG, "Handled exception in TaskHandler invoke.", e); (Prevents duplicate logs)
}
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in .exception handler 2", e)
} }
} }
} }

View file

@ -9,9 +9,7 @@ import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R import com.futo.platformplayer.R
@ -23,21 +21,22 @@ 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
import com.futo.platformplayer.views.adapters.DeviceAdapter import com.futo.platformplayer.views.adapters.DeviceAdapter
import com.futo.platformplayer.views.adapters.DeviceAdapterEntry
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ConnectCastingDialog(context: Context?) : AlertDialog(context) { class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
private lateinit var _imageLoader: ImageView; private lateinit var _imageLoader: ImageView;
private lateinit var _buttonClose: Button; private lateinit var _buttonClose: Button;
private lateinit var _buttonAdd: LinearLayout; private lateinit var _buttonAdd: ImageButton;
private lateinit var _buttonScanQR: LinearLayout; private lateinit var _buttonScanQR: ImageButton;
private lateinit var _textNoDevicesFound: TextView; private lateinit var _textNoDevicesFound: TextView;
private lateinit var _textNoDevicesRemembered: TextView;
private lateinit var _recyclerDevices: RecyclerView; private lateinit var _recyclerDevices: RecyclerView;
private lateinit var _recyclerRememberedDevices: RecyclerView;
private lateinit var _adapter: DeviceAdapter; private lateinit var _adapter: DeviceAdapter;
private val _devices: MutableSet<String> = mutableSetOf() private lateinit var _rememberedAdapter: DeviceAdapter;
private val _rememberedDevices: MutableSet<String> = mutableSetOf() private val _devices: ArrayList<CastingDevice> = arrayListOf();
private val _unifiedDevices: MutableList<DeviceAdapterEntry> = mutableListOf() private val _rememberedDevices: ArrayList<CastingDevice> = arrayListOf();
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -46,40 +45,42 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
_imageLoader = findViewById(R.id.image_loader); _imageLoader = findViewById(R.id.image_loader);
_buttonClose = findViewById(R.id.button_close); _buttonClose = findViewById(R.id.button_close);
_buttonAdd = findViewById(R.id.button_add); _buttonAdd = findViewById(R.id.button_add);
_buttonScanQR = findViewById(R.id.button_qr); _buttonScanQR = findViewById(R.id.button_scan_qr);
_recyclerDevices = findViewById(R.id.recycler_devices); _recyclerDevices = findViewById(R.id.recycler_devices);
_recyclerRememberedDevices = findViewById(R.id.recycler_remembered_devices);
_textNoDevicesFound = findViewById(R.id.text_no_devices_found); _textNoDevicesFound = findViewById(R.id.text_no_devices_found);
_textNoDevicesRemembered = findViewById(R.id.text_no_devices_remembered);
_adapter = DeviceAdapter(_unifiedDevices) _adapter = DeviceAdapter(_devices, false);
_recyclerDevices.adapter = _adapter; _recyclerDevices.adapter = _adapter;
_recyclerDevices.layoutManager = LinearLayoutManager(context); _recyclerDevices.layoutManager = LinearLayoutManager(context);
_adapter.onPin.subscribe { d -> _rememberedAdapter = DeviceAdapter(_rememberedDevices, true);
val isRemembered = _rememberedDevices.contains(d.name) _rememberedAdapter.onRemove.subscribe { d ->
val newIsRemembered = !isRemembered if (StateCasting.instance.activeDevice == d) {
if (newIsRemembered) { d.stopCasting();
StateCasting.instance.addRememberedDevice(d)
val name = d.name
if (name != null) {
_rememberedDevices.add(name)
}
} else {
StateCasting.instance.removeRememberedDevice(d)
_rememberedDevices.remove(d.name)
} }
updateUnifiedList()
StateCasting.instance.removeRememberedDevice(d);
val index = _rememberedDevices.indexOf(d);
if (index != -1) {
_rememberedDevices.removeAt(index);
_rememberedAdapter.notifyItemRemoved(index);
}
_textNoDevicesRemembered.visibility = if (_rememberedDevices.isEmpty()) View.VISIBLE else View.GONE;
_recyclerRememberedDevices.visibility = if (_rememberedDevices.isNotEmpty()) View.VISIBLE else View.GONE;
};
_rememberedAdapter.onConnect.subscribe { _ ->
dismiss()
//UIDialogs.showCastingDialog(context)
} }
//TODO: Integrate remembered into the main list
//TODO: Add green indicator to indicate a device is oneline
//TODO: Add pinning
//TODO: Implement QR code as an option in add manually
//TODO: Remove start button
_adapter.onConnect.subscribe { _ -> _adapter.onConnect.subscribe { _ ->
dismiss() dismiss()
//UIDialogs.showCastingDialog(context) //UIDialogs.showCastingDialog(context)
} }
_recyclerRememberedDevices.adapter = _rememberedAdapter;
_recyclerRememberedDevices.layoutManager = LinearLayoutManager(context);
_buttonClose.setOnClickListener { dismiss(); }; _buttonClose.setOnClickListener { dismiss(); };
_buttonAdd.setOnClickListener { _buttonAdd.setOnClickListener {
@ -103,111 +104,78 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
super.show(); super.show();
Logger.i(TAG, "Dialog shown."); Logger.i(TAG, "Dialog shown.");
StateCasting.instance.startDiscovering()
(_imageLoader.drawable as Animatable?)?.start(); (_imageLoader.drawable as Animatable?)?.start();
synchronized(StateCasting.instance.devices) { _devices.clear();
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name }) synchronized (StateCasting.instance.devices) {
_devices.addAll(StateCasting.instance.devices.values);
} }
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames()) _rememberedDevices.clear();
updateUnifiedList() synchronized (StateCasting.instance.rememberedDevices) {
_rememberedDevices.addAll(StateCasting.instance.rememberedDevices);
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
val name = d.name
if (name != null)
_devices.add(name)
updateUnifiedList()
}
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name == d.name }
if (index != -1) {
_unifiedDevices[index] = DeviceAdapterEntry(d, _unifiedDevices[index].isPinnedDevice, _unifiedDevices[index].isOnlineDevice)
_adapter.notifyItemChanged(index)
}
}
StateCasting.instance.onDeviceRemoved.subscribe(this) { d ->
_devices.remove(d.name)
updateUnifiedList()
}
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
if (connectionState == CastConnectionState.CONNECTED) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
dismiss()
}
}
} }
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE; _textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE; _recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
_textNoDevicesRemembered.visibility = if (_rememberedDevices.isEmpty()) View.VISIBLE else View.GONE;
_recyclerRememberedDevices.visibility = if (_rememberedDevices.isNotEmpty()) View.VISIBLE else View.GONE;
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
_devices.add(d);
_adapter.notifyItemInserted(_devices.size - 1);
_textNoDevicesFound.visibility = View.GONE;
_recyclerDevices.visibility = View.VISIBLE;
};
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
val index = _devices.indexOf(d);
if (index == -1) {
return@subscribe;
}
_devices[index] = d;
_adapter.notifyItemChanged(index);
};
StateCasting.instance.onDeviceRemoved.subscribe(this) { d ->
val index = _devices.indexOf(d);
if (index == -1) {
return@subscribe;
}
_devices.removeAt(index);
_adapter.notifyItemRemoved(index);
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
};
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
if (connectionState != CastConnectionState.CONNECTED) {
return@subscribe;
}
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
dismiss();
};
};
_adapter.notifyDataSetChanged();
_rememberedAdapter.notifyDataSetChanged();
} }
override fun dismiss() { override fun dismiss() {
super.dismiss() super.dismiss();
(_imageLoader.drawable as Animatable?)?.stop()
StateCasting.instance.onDeviceAdded.remove(this)
StateCasting.instance.onDeviceChanged.remove(this)
StateCasting.instance.onDeviceRemoved.remove(this)
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this)
}
private fun updateUnifiedList() { (_imageLoader.drawable as Animatable?)?.stop();
val oldList = ArrayList(_unifiedDevices)
val newList = buildUnifiedList()
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() { StateCasting.instance.stopDiscovering()
override fun getOldListSize() = oldList.size StateCasting.instance.onDeviceAdded.remove(this);
override fun getNewListSize() = newList.size StateCasting.instance.onDeviceChanged.remove(this);
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { StateCasting.instance.onDeviceRemoved.remove(this);
val oldItem = oldList[oldItemPosition] StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
val newItem = newList[newItemPosition]
return oldItem.castingDevice.name == newItem.castingDevice.name
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return oldItem.castingDevice.name == newItem.castingDevice.name
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
}
})
_unifiedDevices.clear()
_unifiedDevices.addAll(newList)
diffResult.dispatchUpdatesTo(_adapter)
_textNoDevicesFound.visibility = if (_unifiedDevices.isEmpty()) View.VISIBLE else View.GONE
_recyclerDevices.visibility = if (_unifiedDevices.isNotEmpty()) View.VISIBLE else View.GONE
}
private fun buildUnifiedList(): List<DeviceAdapterEntry> {
val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
val rememberedDevices = StateCasting.instance.getRememberedCastingDevices().associateBy { it.name }
val unifiedList = mutableListOf<DeviceAdapterEntry>()
val intersectionNames = _devices.intersect(_rememberedDevices)
for (name in intersectionNames) {
onlineDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, true, true)) }
}
val onlineOnlyNames = _devices - _rememberedDevices
for (name in onlineOnlyNames) {
onlineDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, false, true)) }
}
val rememberedOnlyNames = _rememberedDevices - _devices
for (name in rememberedOnlyNames) {
rememberedDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, true, false)) }
}
return unifiedList
} }
companion object { companion object {

View file

@ -6,16 +6,12 @@ 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;
@ -45,17 +41,8 @@ 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") {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
val zipBytes = it?.readBytes(context) ?: return@launch; StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
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 {
@ -64,35 +51,17 @@ 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") {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
val txtBytes = it?.readBytes(context) ?: return@launch; val txt = String(txtBytes);
val txt = String(txtBytes); StateBackup.importTxt(_context, txt);
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") {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
val jsonBytes = it?.readBytes(context) ?: return@launch; val json = String(jsonBytes);
val json = String(jsonBytes); StateBackup.importNewPipeSubs(_context, json);
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 {

View file

@ -47,7 +47,6 @@ import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.toHumanBitrate import com.futo.platformplayer.toHumanBitrate
import com.futo.platformplayer.toHumanBytesSpeed import com.futo.platformplayer.toHumanBytesSpeed
import com.futo.polycentric.core.hexStringToByteArray
import hasAnySource import hasAnySource
import isDownloadable import isDownloadable
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -60,21 +59,16 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.lang.Thread.sleep import java.lang.Thread.sleep
import java.nio.ByteBuffer
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.UUID import java.util.UUID
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask import java.util.concurrent.ForkJoinTask
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.time.times import kotlin.time.times
@ -570,14 +564,6 @@ class VideoDownload {
} }
} }
private fun decryptSegment(encryptedSegment: ByteArray, key: ByteArray, iv: ByteArray): ByteArray {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val secretKey = SecretKeySpec(key, "AES")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
return cipher.doFinal(encryptedSegment)
}
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long { private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
if(targetFile.exists()) if(targetFile.exists())
targetFile.delete(); targetFile.delete();
@ -593,14 +579,6 @@ class VideoDownload {
?: throw Exception("Variant playlist content is empty") ?: throw Exception("Variant playlist content is empty")
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl) val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
val decryptionInfo: DecryptionInfo? = if (variantPlaylist.decryptionInfo != null) {
val keyResponse = client.get(variantPlaylist.decryptionInfo.keyUrl)
check(keyResponse.isOk) { "HLS request failed for decryption key: ${keyResponse.code}" }
DecryptionInfo(keyResponse.body!!.bytes(), variantPlaylist.decryptionInfo.iv?.hexStringToByteArray())
} else {
null
}
variantPlaylist.segments.forEachIndexed { index, segment -> variantPlaylist.segments.forEachIndexed { index, segment ->
if (segment !is HLS.MediaSegment) { if (segment !is HLS.MediaSegment) {
return@forEachIndexed return@forEachIndexed
@ -612,7 +590,7 @@ class VideoDownload {
try { try {
segmentFiles.add(segmentFile) segmentFiles.add(segmentFile)
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri, if (index == 0) null else decryptionInfo, index) { segmentLength, totalRead, lastSpeed -> val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed ->
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed) onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
@ -652,8 +630,10 @@ class VideoDownload {
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) { private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
val cmd = val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt")
"-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\"" fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" })
val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\""
val statisticsCallback = StatisticsCallback { _ -> val statisticsCallback = StatisticsCallback { _ ->
//TODO: Show progress? //TODO: Show progress?
@ -663,6 +643,7 @@ class VideoDownload {
val session = FFmpegKit.executeAsync(cmd, val session = FFmpegKit.executeAsync(cmd,
{ session -> { session ->
if (ReturnCode.isSuccess(session.returnCode)) { if (ReturnCode.isSuccess(session.returnCode)) {
fileList.delete()
continuation.resumeWith(Result.success(Unit)) continuation.resumeWith(Result.success(Unit))
} else { } else {
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) { val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
@ -670,6 +651,7 @@ class VideoDownload {
} else { } else {
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}" "Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
} }
fileList.delete()
continuation.resumeWithException(RuntimeException(errorMessage)) continuation.resumeWithException(RuntimeException(errorMessage))
} }
}, },
@ -724,7 +706,7 @@ class VideoDownload {
val t = cue.groupValues[1]; val t = cue.groupValues[1];
val d = cue.groupValues[2]; val d = cue.groupValues[2];
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString()); val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
val data = if(executor != null) val data = if(executor != null)
executor.executeRequest("GET", url, null, mapOf()); executor.executeRequest("GET", url, null, mapOf());
@ -789,7 +771,7 @@ class VideoDownload {
else { else {
Logger.i(TAG, "Download $name Sequential"); Logger.i(TAG, "Download $name Sequential");
try { try {
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, null, 0, onProgress); sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)") Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
throw e throw e
@ -816,31 +798,7 @@ class VideoDownload {
} }
return sourceLength!!; return sourceLength!!;
} }
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
data class DecryptionInfo(
val key: ByteArray,
val iv: ByteArray?
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DecryptionInfo
if (!key.contentEquals(other.key)) return false
if (!iv.contentEquals(other.iv)) return false
return true
}
override fun hashCode(): Int {
var result = key.contentHashCode()
result = 31 * result + iv.contentHashCode()
return result
}
}
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, decryptionInfo: DecryptionInfo?, index: Int, onProgress: (Long, Long, Long) -> Unit): Long {
val progressRate: Int = 4096 * 5; val progressRate: Int = 4096 * 5;
var lastProgressCount: Int = 0; var lastProgressCount: Int = 0;
val speedRate: Int = 4096 * 5; val speedRate: Int = 4096 * 5;
@ -860,8 +818,6 @@ class VideoDownload {
val sourceLength = result.body.contentLength(); val sourceLength = result.body.contentLength();
val sourceStream = result.body.byteStream(); val sourceStream = result.body.byteStream();
val segmentBuffer = ByteArrayOutputStream()
var totalRead: Long = 0; var totalRead: Long = 0;
try { try {
var read: Int; var read: Int;
@ -872,7 +828,7 @@ class VideoDownload {
if (read < 0) if (read < 0)
break; break;
segmentBuffer.write(buffer, 0, read); fileStream.write(buffer, 0, read);
totalRead += read; totalRead += read;
@ -898,21 +854,6 @@ class VideoDownload {
result.body.close() result.body.close()
} }
if (decryptionInfo != null) {
var iv = decryptionInfo.iv
if (iv == null) {
iv = ByteBuffer.allocate(16)
.putLong(0L)
.putLong(index.toLong())
.array()
}
val decryptedData = decryptSegment(segmentBuffer.toByteArray(), decryptionInfo.key, iv!!)
fileStream.write(decryptedData)
} else {
fileStream.write(segmentBuffer.toByteArray())
}
onProgress(sourceLength, totalRead, 0); onProgress(sourceLength, totalRead, 0);
return sourceLength; return sourceLength;
} }
@ -1219,8 +1160,6 @@ class VideoDownload {
fun audioContainerToExtension(container: String): String { fun audioContainerToExtension(container: String): String {
if (container.contains("audio/mp4")) if (container.contains("audio/mp4"))
return "mp4a"; return "mp4a";
else if (container.contains("video/mp4"))
return "mp4";
else if (container.contains("audio/mpeg")) else if (container.contains("audio/mpeg"))
return "mpga"; return "mpga";
else if (container.contains("audio/mp3")) else if (container.contains("audio/mp3"))
@ -1228,7 +1167,7 @@ class VideoDownload {
else if (container.contains("audio/webm")) else if (container.contains("audio/webm"))
return "webm"; return "webm";
else if (container == "application/vnd.apple.mpegurl") else if (container == "application/vnd.apple.mpegurl")
return "m4a"; return "mp4a";
else else
return "audio";// throw IllegalStateException("Unknown container: " + container) return "audio";// throw IllegalStateException("Unknown container: " + container)
} }

View file

@ -69,7 +69,7 @@ class VideoExport {
outputFile = f; outputFile = f;
} else if (v != null) { } else if (v != null) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container); val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
val f = downloadRoot.createFile(if (v.container == "application/vnd.apple.mpegurl") "video/mp4" else v.container, outputFileName) val f = downloadRoot.createFile(v.container, outputFileName)
?: throw Exception("Failed to create file in external directory."); ?: throw Exception("Failed to create file in external directory.");
Logger.i(TAG, "Copying video."); Logger.i(TAG, "Copying video.");
@ -81,8 +81,8 @@ class VideoExport {
outputFile = f; outputFile = f;
} else if (a != null) { } else if (a != null) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container); val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") "video/mp4" else a.container, outputFileName) val f = downloadRoot.createFile(a.container, outputFileName)
?: throw Exception("Failed to create file in external directory."); ?: throw Exception("Failed to create file in external directory.");
Logger.i(TAG, "Copying audio."); Logger.i(TAG, "Copying audio.");

View file

@ -6,13 +6,13 @@ import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.interop.V8Runtime
import com.caoccao.javet.interop.options.V8Flags
import com.caoccao.javet.interop.options.V8RuntimeOptions
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
@ -26,7 +26,6 @@ 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
@ -39,18 +38,8 @@ import com.futo.platformplayer.engine.packages.V8Package
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets import com.futo.platformplayer.states.StateAssets
import com.futo.platformplayer.toList
import com.futo.platformplayer.toV8ValueBlocking
import com.futo.platformplayer.toV8ValueAsync
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.cancel
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class V8Plugin { class V8Plugin {
val config: IV8PluginConfig; val config: IV8PluginConfig;
@ -58,14 +47,10 @@ 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);
@ -82,8 +67,10 @@ class V8Plugin {
var isStopped = true; var isStopped = true;
val onStopped = Event1<V8Plugin>(); val onStopped = Event1<V8Plugin>();
private val _busyLock = ReentrantLock() //TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial
val isBusy get() = _busyLock.isLocked; private val _busyCounterLock = Object();
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) {
@ -153,7 +140,6 @@ 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();
@ -162,8 +148,6 @@ 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();
@ -200,23 +184,11 @@ class V8Plugin {
} }
fun stop(){ fun stop(){
Logger.i(TAG, "Stopping plugin [${config.name}]"); Logger.i(TAG, "Stopping plugin [${config.name}]");
busy { isStopped = true;
Logger.i(TAG, "Plugin stopping"); whenNotBusy {
synchronized(_runtimeLock) { synchronized(_runtimeLock) {
if(isStopped)
return@busy;
isStopped = true; isStopped = true;
runtimeId = runtimeId + 1;
//Cleanup http
for(pack in _depsPackages) {
if(pack is PackageHttp) {
pack.cleanup();
}
}
_runtime?.let { _runtime?.let {
_runtimeMap.remove(it);
_runtime = null; _runtime = null;
if(!it.isClosed && !it.isDead) { if(!it.isClosed && !it.isDead) {
try { try {
@ -231,146 +203,61 @@ class V8Plugin {
Logger.i(TAG, "Stopped plugin [${config.name}]"); Logger.i(TAG, "Stopped plugin [${config.name}]");
}; };
} }
Logger.i(TAG, "Plugin stopped");
onStopped.emit(this); onStopped.emit(this);
} }
cancelAllPromises();
} }
fun isThreadAlreadyBusy(): Boolean {
return _busyLock.isHeldByCurrentThread;
}
fun <T> busy(handle: ()->T): T {
_busyLock.lock();
try {
return handle();
}
finally {
_busyLock.unlock();
}
/*
_busyLock.withLock {
//Logger.i(TAG, "Entered busy: " + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString());
return handle();
}*/
}
fun <T> unbusy(handle: ()->T): T {
val wasLocked = isThreadAlreadyBusy();
if(!wasLocked)
return handle();
val lockCount = _busyLock.holdCount;
for(i in 1..lockCount)
_busyLock.unlock();
try {
Logger.w(TAG, "Unlocking V8 thread for [${config.name}] for a blocking resolve of a promise")
return handle();
}
finally {
Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise")
for(i in 1..lockCount)
_busyLock.lock();
}
}
fun execute(js: String) : V8Value { fun execute(js: String) : V8Value {
return executeTyped<V8Value>(js); return executeTyped<V8Value>(js);
} }
suspend fun <T : V8Value> executeTypedAsync(js: String) : Deferred<T> {
warnIfMainThread("V8Plugin.executeTypedAsync");
if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js);
return withContext(IO) {
return@withContext busy {
try {
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
val result = catchScriptErrors<V8Value>("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute()
};
if (result is V8ValuePromise) {
return@busy result.toV8ValueAsync<T>(this@V8Plugin);
} else
return@busy CompletableDeferred(result as T);
}
catch(ex: Throwable) {
val def = CompletableDeferred<T>();
def.completeExceptionally(ex);
return@busy def;
}
}
}
}
fun <T : V8Value> executeTyped(js: String) : T { fun <T : V8Value> executeTyped(js: String) : T {
warnIfMainThread("V8Plugin.executeTyped"); warnIfMainThread("V8Plugin.executeTyped");
if(isStopped) if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js); throw PluginEngineStoppedException(config, "Instance is stopped", js);
val result = busy { synchronized(_busyCounterLock) {
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); _busyCounter++;
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);
} }
return result as T; finally {
} synchronized(_busyCounterLock) {
fun executeBoolean(js: String) : Boolean? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value } } //Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
fun executeString(js: String) : String? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value } } try {
fun executeInteger(js: String) : Int? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value } } afterBusy.emit(_busyCounter - 1);
}
catch(ex: Throwable) {
fun <T: V8Value> handlePromise(result: V8ValuePromise): CompletableDeferred<T> { Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex);
val def = CompletableDeferred<T>(); }
result.register(object: IV8ValuePromise.IListener { _busyCounter--;
override fun onFulfilled(p0: V8Value?) {
resolvePromise(result);
def.complete(p0 as T);
} }
override fun onRejected(p0: V8Value?) {
resolvePromise(result);
def.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
}
override fun onCatch(p0: V8Value?) {
resolvePromise(result);
def.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
}
});
registerPromise(result) {
if(def.isActive)
def.cancel("Cancelled by system");
}
return def;
}
fun registerPromise(promise: V8ValuePromise, onCancelled: ((V8ValuePromise)->Unit)? = null) {
Logger.v(TAG, "Promise registered for plugin [${config.name}]: ${promise.hashCode()}");
if (onCancelled != null) {
_promises.put(promise, onCancelled)
};
}
fun resolvePromise(promise: V8ValuePromise, cancelled: Boolean = false) {
Logger.v(TAG, "Promise resolved for plugin [${config.name}]: ${promise.hashCode()}");
val found = synchronized(_promises) {
val found = _promises.getOrDefault(promise, null);
_promises.remove(promise);
return@synchronized found;
};
if(found != null && cancelled)
found(promise);
}
fun cancelAllPromises(){
val promises = _promises.keys().toList();
for(key in promises) {
try {
resolvePromise(key, true);
}
catch(ex: Throwable) {}
} }
} }
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value };
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value };
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value };
fun whenNotBusy(handler: (V8Plugin)->Unit) {
synchronized(_busyCounterLock) {
if(_busyCounter == 0)
handler(this);
else {
val tag = Object();
afterBusy.subscribe(tag) {
if(it == 0) {
Logger.w(TAG, "V8Plugin afterBusy handled");
afterBusy.remove(tag);
handler(this);
}
}
}
}
}
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?
@ -397,14 +284,8 @@ 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
@ -438,23 +319,14 @@ 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) {
val obj = executeEx.scriptingError?.context if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) {
if(obj != null && obj.containsKey("plugin_type") == true) { val pluginType = executeEx.scriptingError.context["plugin_type"].toString();
val pluginType = obj["plugin_type"].toString();
//Captcha //Captcha
if (pluginType == "CaptchaRequiredException") { if (pluginType == "CaptchaRequiredException") {
throw ScriptCaptchaRequiredException(config, throw ScriptCaptchaRequiredException(config,
obj["url"]?.toString(), executeEx.scriptingError.context["url"]?.toString(),
obj["body"]?.toString(), executeEx.scriptingError.context["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);
} }
@ -468,41 +340,6 @@ 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) {
@ -553,4 +390,9 @@ 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)
*/
} }

View file

@ -2,7 +2,6 @@ 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) {
@ -12,7 +11,6 @@ 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"));
} }
} }

View file

@ -2,7 +2,6 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
@ -12,7 +11,6 @@ open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Except
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException")); return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException"));
} }
} }

View file

@ -2,7 +2,6 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@ -10,7 +9,6 @@ class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String?,
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
val contextName = "ScriptCaptchaRequiredException"; val contextName = "ScriptCaptchaRequiredException";
return ScriptCaptchaRequiredException(config, return ScriptCaptchaRequiredException(config,
obj.getOrDefault<String>(config, "url", contextName, null), obj.getOrDefault<String>(config, "url", contextName, null),

View file

@ -2,14 +2,12 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) { class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException {
obj.ensureIsBusy();
return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException")); return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException"));
} }
} }

View file

@ -2,7 +2,6 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
@ -12,7 +11,6 @@ open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: E
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException")); return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException"));
} }
} }

View file

@ -2,7 +2,6 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptExecutionException(config, error, ex, stack, code) { open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptExecutionException(config, error, ex, stack, code) {
@ -12,7 +11,6 @@ open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException")); return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException"));
} }
} }

View file

@ -2,7 +2,6 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex: Exception? = null, val stack: String? = null, code: String? = null) : PluginException(config, error, ex, code) { open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex: Exception? = null, val stack: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
@ -12,7 +11,6 @@ open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex:
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException {
obj.ensureIsBusy();
return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException")); return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException"));
} }
} }

View file

@ -2,14 +2,12 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptImplementationException(config: IV8PluginConfig, error: String, ex: Exception? = null, var pluginId: String? = null, code: String? = null) : PluginException(config, error, ex, code) { class ScriptImplementationException(config: IV8PluginConfig, error: String, ex: Exception? = null, var pluginId: String? = null, code: String? = null) : PluginException(config, error, ex, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException {
obj.ensureIsBusy();
return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException")); return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException"));
} }
} }

View file

@ -2,14 +2,12 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException")); return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException"));
} }
} }

View file

@ -1,22 +0,0 @@
package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
class ScriptReloadRequiredException(config: IV8PluginConfig, val msg: String?, val reloadData: String?, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, msg ?: "ReloadRequired", ex, stack, code) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
val contextName = "ScriptReloadRequiredException";
return ScriptReloadRequiredException(config,
obj.getOrThrow(config, "message", contextName),
obj.getOrDefault<String>(config, "reloadData", contextName, null));
}
}
}

View file

@ -2,13 +2,11 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) { class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException {
obj.ensureIsBusy();
return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException")); return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException"));
} }
} }

View file

@ -2,14 +2,12 @@ package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class ScriptUnavailableException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { class ScriptUnavailableException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
obj.ensureIsBusy();
return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException")); return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException"));
} }
} }

View file

@ -13,8 +13,8 @@ open class V8BindObject : IV8Convertable {
override fun toV8(runtime: V8Runtime): V8Value? { override fun toV8(runtime: V8Runtime): V8Value? {
synchronized(this) { synchronized(this) {
//if(_runtimeObj != null) if(_runtimeObj != null)
// return _runtimeObj; return _runtimeObj;
val v8Obj = runtime.createV8ValueObject(); val v8Obj = runtime.createV8ValueObject();
v8Obj.bind(this); v8Obj.bind(this);

View file

@ -4,7 +4,6 @@ import android.media.MediaCodec
import android.media.MediaCodecList import android.media.MediaCodecList
import com.caoccao.javet.annotations.V8Function import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property import com.caoccao.javet.annotations.V8Property
import com.caoccao.javet.interop.callback.JavetCallbackContext
import com.caoccao.javet.utils.JavetResourceUtils import com.caoccao.javet.utils.JavetResourceUtils
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueFunction import com.caoccao.javet.values.reference.V8ValueFunction
@ -13,7 +12,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
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.contents.ContentType
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.JSClientConstants import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
@ -27,7 +25,6 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.concurrent.ConcurrentHashMap
class PackageBridge : V8Package { class PackageBridge : V8Package {
@Transient @Transient
@ -75,35 +72,6 @@ class PackageBridge : V8Package {
fun buildSpecVersion(): Int { fun buildSpecVersion(): Int {
return JSClientConstants.PLUGIN_SPEC_VERSION; return JSClientConstants.PLUGIN_SPEC_VERSION;
} }
@V8Property
fun buildPlatform(): String {
return "android";
}
@V8Property
fun supportedFeatures(): Array<String> {
return arrayOf(
"ReloadRequiredException",
"HttpBatchClient",
"Async"
);
}
@V8Property
fun supportedContent(): Array<Int> {
return arrayOf(
ContentType.MEDIA.value,
ContentType.POST.value,
ContentType.PLAYLIST.value,
ContentType.WEB.value,
ContentType.URL.value,
ContentType.NESTED_VIDEO.value,
ContentType.CHANNEL.value,
ContentType.LOCKED.value,
ContentType.PLACEHOLDER.value,
ContentType.DEFERRED.value
)
}
@V8Function @V8Function
fun dispose(value: V8Value) { fun dispose(value: V8Value) {
@ -112,54 +80,45 @@ class PackageBridge : V8Package {
} }
var timeoutCounter = 0; var timeoutCounter = 0;
var timeoutMap = ConcurrentHashMap<Int, Any?>(); var timeoutMap = HashSet<Int>();
@V8Function @V8Function
fun setTimeout(func: V8ValueFunction, timeout: Long): Int { fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
val id = timeoutCounter++; val id = timeoutCounter++;
val funcClone = func.toClone<V8ValueFunction>() val funcClone = func.toClone<V8ValueFunction>()
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
delay(timeout); delay(timeout);
if (_plugin.isStopped) synchronized(timeoutMap) {
return@launch; if(!timeoutMap.contains(id)) {
if (!timeoutMap.containsKey(id)) { JavetResourceUtils.safeClose(funcClone);
_plugin.busy { return@launch;
if (!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone);
} }
return@launch; timeoutMap.remove(id);
} }
timeoutMap.remove(id);
try { try {
Logger.w(TAG, "setTimeout before busy (${timeout}): ${_plugin.isBusy}"); _plugin.whenNotBusy {
_plugin.busy { funcClone.callVoid(null, arrayOf<Any>());
Logger.w(TAG, "setTimeout in busy");
if (!_plugin.isStopped)
funcClone.callVoid(null, arrayOf<Any>());
Logger.w(TAG, "setTimeout after");
} }
} catch (ex: Throwable) { }
catch(ex: Throwable) {
Logger.e(TAG, "Failed timeout callback", ex); Logger.e(TAG, "Failed timeout callback", ex);
} finally { }
_plugin.busy { finally {
if (!_plugin.isStopped) JavetResourceUtils.safeClose(funcClone);
JavetResourceUtils.safeClose(funcClone);
}
//_plugin.whenNotBusy {
//}
} }
}; };
timeoutMap.put(id, true); synchronized(timeoutMap) {
timeoutMap.add(id);
}
return id; return id;
} }
@V8Function @V8Function
fun clearTimeout(id: Int) { fun clearTimeout(id: Int) {
if (timeoutMap.containsKey(id)) synchronized(timeoutMap) {
timeoutMap.remove(id); if(timeoutMap.contains(id))
} timeoutMap.remove(id);
@V8Function }
fun sleep(length: Int) {
Thread.sleep(length.toLong());
} }
@V8Function @V8Function
@ -167,7 +126,7 @@ class PackageBridge : V8Package {
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}"); Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try { try {
UIDialogs.appToast(str); UIDialogs.toast(str);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e); Logger.e(TAG, "Failed to show toast.", e);
} }

View file

@ -8,7 +8,9 @@ import com.caoccao.javet.enums.V8ProxyMode
import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.interop.V8Runtime
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.V8ValueArrayBuffer
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValueSharedArrayBuffer
import com.caoccao.javet.values.reference.V8ValueTypedArray import com.caoccao.javet.values.reference.V8ValueTypedArray
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
@ -18,9 +20,15 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.internal.IV8Convertable import com.futo.platformplayer.engine.internal.IV8Convertable
import com.futo.platformplayer.engine.internal.V8BindObject import com.futo.platformplayer.engine.internal.V8BindObject
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask import java.util.concurrent.ForkJoinTask
import kotlin.concurrent.thread
import kotlin.streams.asSequence
class PackageHttp: V8Package { class PackageHttp: V8Package {
@Transient @Transient
@ -41,20 +49,6 @@ class PackageHttp: V8Package {
private var _batchPoolLock: Any = Any(); private var _batchPoolLock: Any = Any();
private var _batchPool: ForkJoinPool? = null; private var _batchPool: ForkJoinPool? = null;
private val aliveSockets = mutableListOf<SocketResult>();
private var _cleanedUp = false;
private val _clients = mutableMapOf<String, PackageHttpClient>()
fun getClient(id: String?): PackageHttpClient {
if(id == null)
throw IllegalArgumentException("Http client ${id} doesn't exist");
if(_packageClient.clientId() == id)
return _packageClient;
if(_packageClientAuth.clientId() == id)
return _packageClientAuth;
return _clients.getOrDefault(id, null) ?: throw IllegalArgumentException("Http client ${id} doesn't exist");
}
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) { constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
_config = config; _config = config;
@ -64,27 +58,6 @@ class PackageHttp: V8Package {
_packageClientAuth = PackageHttpClient(this, _clientAuth); _packageClientAuth = PackageHttpClient(this, _clientAuth);
} }
fun cleanup(){
Logger.w(TAG, "PackageHttp Cleaning up")
val sockets = synchronized(aliveSockets) { aliveSockets.toList() }
_cleanedUp = true;
for(socket in sockets){
try {
Logger.w(TAG, "PackageHttp Socket Cleaned Up");
socket.close(1001, "Cleanup");
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to close socket", ex);
}
}
if(sockets.size > 0) {
//Thread.sleep(100); //Give sockets a bit
}
synchronized(aliveSockets) {
aliveSockets.clear();
}
}
/* /*
Automatically adjusting threadpool dedicated per PackageHttp for batch requests. Automatically adjusting threadpool dedicated per PackageHttp for batch requests.
@ -123,8 +96,6 @@ class PackageHttp: V8Package {
_plugin.registerHttpClient(httpClient); _plugin.registerHttpClient(httpClient);
val client = PackageHttpClient(this, httpClient); val client = PackageHttpClient(this, httpClient);
_clients.put(client.clientId() ?: "", client);
return client; return client;
} }
@V8Function @V8Function
@ -140,24 +111,24 @@ class PackageHttp: V8Package {
@V8Function @V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse { fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
return if(useAuth) return if(useAuth)
_packageClientAuth.requestInternal(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING) _packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
else else
_packageClient.requestInternal(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING); _packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
} }
@V8Function @V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse { fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
return if(useAuth) return if(useAuth)
_packageClientAuth.requestWithBodyInternal(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING) _packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
else else
_packageClient.requestWithBodyInternal(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING); _packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
} }
@V8Function @V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse { fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
return if(useAuth) return if(useAuth)
_packageClientAuth.GETInternal(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING) _packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
else else
_packageClient.GETInternal(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING); _packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
} }
@V8Function @V8Function
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse { fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
@ -165,15 +136,15 @@ class PackageHttp: V8Package {
val client = if(useAuth) _packageClientAuth else _packageClient; val client = if(useAuth) _packageClientAuth else _packageClient;
if(body is V8ValueString) if(body is V8ValueString)
return client.POSTInternal(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING); return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is String) else if(body is String)
return client.POSTInternal(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING); return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is V8ValueTypedArray) else if(body is V8ValueTypedArray)
return client.POSTInternal(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING); return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is ByteArray) else if(body is ByteArray)
return client.POSTInternal(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING); return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is ArrayList<*>) //Avoid this case, used purely for testing else if(body is ArrayList<*>) //Avoid this case, used purely for testing
return client.POSTInternal(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING); return client.POST(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else else
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST"); throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
} }
@ -259,18 +230,18 @@ class PackageHttp: V8Package {
@V8Function @V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder { fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers); return clientRequest(_package.getDefaultClient(useAuth), method, url, headers);
} }
@V8Function @V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder { fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequestWithBody(_package.getDefaultClient(useAuth).clientId(), method, url, body, headers); return clientRequestWithBody(_package.getDefaultClient(useAuth), method, url, body, headers);
} }
@V8Function @V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers); = clientGET(_package.getDefaultClient(useAuth), url, headers);
@V8Function @V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers); = clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
@V8Function @V8Function
fun DUMMY(): BatchBuilder { fun DUMMY(): BatchBuilder {
@ -281,21 +252,21 @@ class PackageHttp: V8Package {
//Client-specific //Client-specific
@V8Function @V8Function
fun clientRequest(clientId: String?, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder { fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers))); _reqs.add(Pair(client, RequestDescriptor(method, url, headers)));
return BatchBuilder(_package, _reqs); return BatchBuilder(_package, _reqs);
} }
@V8Function @V8Function
fun clientRequestWithBody(clientId: String?, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder { fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers, body))); _reqs.add(Pair(client, RequestDescriptor(method, url, headers, body)));
return BatchBuilder(_package, _reqs); return BatchBuilder(_package, _reqs);
} }
@V8Function @V8Function
fun clientGET(clientId: String?, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequest(clientId, "GET", url, headers); = clientRequest(client, "GET", url, headers);
@V8Function @V8Function
fun clientPOST(clientId: String?, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequestWithBody(clientId, "POST", url, body, headers); = clientRequestWithBody(client, "POST", url, body, headers);
//Finalizer //Finalizer
@ -305,9 +276,9 @@ class PackageHttp: V8Package {
if(it.second.method == "DUMMY") if(it.second.method == "DUMMY")
return@autoParallelPool null; return@autoParallelPool null;
if(it.second.body != null) if(it.second.body != null)
return@autoParallelPool it.first.requestWithBodyInternal(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType); return@autoParallelPool it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
else else
return@autoParallelPool it.first.requestInternal(it.second.method, it.second.url, it.second.headers, it.second.respType); return@autoParallelPool it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
}.map { }.map {
if(it.second != null) if(it.second != null)
throw it.second!!; throw it.second!!;
@ -334,7 +305,6 @@ class PackageHttp: V8Package {
@Transient @Transient
private val _clientId: String?; private val _clientId: String?;
@V8Property @V8Property
fun clientId(): String? { fun clientId(): String? {
return _clientId; return _clientId;
@ -347,17 +317,6 @@ class PackageHttp: V8Package {
_clientId = if(_client is JSHttpClient) _client.clientId else null; _clientId = if(_client is JSHttpClient) _client.clientId else null;
} }
@V8Function
fun resetAuthCookies(){
if(_client is JSHttpClient)
_client.resetAuthCookies();
}
@V8Function
fun clearOtherCookies(){
if(_client is JSHttpClient)
_client.clearOtherCookies();
}
@V8Function @V8Function
fun setDefaultHeaders(defaultHeaders: Map<String, String>) { fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
for(pair in defaultHeaders) for(pair in defaultHeaders)
@ -386,9 +345,7 @@ class PackageHttp: V8Package {
} }
@V8Function @V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
= requestInternal(method, url, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
fun requestInternal(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
return@logExceptions catchHttp { return@logExceptions catchHttp {
@ -407,9 +364,7 @@ class PackageHttp: V8Package {
}; };
} }
@V8Function @V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
= requestWithBodyInternal(method, url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun requestWithBodyInternal(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
catchHttp { catchHttp {
@ -430,9 +385,7 @@ class PackageHttp: V8Package {
} }
@V8Function @V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse fun GET(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
= GETInternal(url, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun GETInternal(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
catchHttp { catchHttp {
@ -454,24 +407,7 @@ class PackageHttp: V8Package {
}; };
} }
@V8Function @V8Function
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse { fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
if(body is V8ValueString)
return POSTInternal(url, body.value, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is String)
return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is V8ValueTypedArray)
return POSTInternal(url, body.toBytes(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is ByteArray)
return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
return POSTInternal(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
}
// = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun POSTInternal(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
catchHttp { catchHttp {
@ -492,7 +428,8 @@ class PackageHttp: V8Package {
} }
}; };
} }
fun POSTInternal(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { @V8Function
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
catchHttp { catchHttp {
@ -516,16 +453,9 @@ class PackageHttp: V8Package {
@V8Function @V8Function
fun socket(url: String, headers: Map<String, String>? = null): SocketResult { fun socket(url: String, headers: Map<String, String>? = null): SocketResult {
if(_package._cleanedUp)
throw IllegalStateException("Plugin shutdown");
val socketHeaders = headers?.toMutableMap() ?: HashMap(); val socketHeaders = headers?.toMutableMap() ?: HashMap();
applyDefaultHeaders(socketHeaders); applyDefaultHeaders(socketHeaders);
val socket = SocketResult(_package, this, _client, url, socketHeaders); return SocketResult(this, _client, url, socketHeaders);
Logger.w(TAG, "PackageHttp Socket opened");
synchronized(_package.aliveSockets) {
_package.aliveSockets.add(socket);
}
return socket;
} }
private fun applyDefaultHeaders(headerMap: MutableMap<String, String>) { private fun applyDefaultHeaders(headerMap: MutableMap<String, String>) {
@ -631,15 +561,13 @@ class PackageHttp: V8Package {
private var _listeners: V8ValueObject? = null; private var _listeners: V8ValueObject? = null;
private val _package: PackageHttp;
private val _packageClient: PackageHttpClient; private val _packageClient: PackageHttpClient;
private val _client: ManagedHttpClient; private val _client: ManagedHttpClient;
private val _url: String; private val _url: String;
private val _headers: Map<String, String>; private val _headers: Map<String, String>;
constructor(parent: PackageHttp, pack: PackageHttpClient, client: ManagedHttpClient, url: String, headers: Map<String,String>) { constructor(pack: PackageHttpClient, client: ManagedHttpClient, url: String, headers: Map<String,String>) {
_packageClient = pack; _packageClient = pack;
_package = parent;
_client = client; _client = client;
_url = url; _url = url;
_headers = headers; _headers = headers;
@ -665,11 +593,9 @@ class PackageHttp: V8Package {
override fun open() { override fun open() {
Logger.i(TAG, "Websocket opened: " + _url); Logger.i(TAG, "Websocket opened: " + _url);
_isOpen = true; _isOpen = true;
if(hasOpen && _listeners?.isClosed != true) { if(hasOpen) {
try { try {
_package._plugin.busy { _listeners?.invokeVoid("open", arrayOf<Any>());
_listeners?.invokeVoid("open", arrayOf<Any>());
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
@ -677,22 +603,18 @@ class PackageHttp: V8Package {
} }
} }
override fun message(msg: String) { override fun message(msg: String) {
if(hasMessage && _listeners?.isClosed != true) { if(hasMessage) {
try { try {
_package._plugin.busy { _listeners?.invokeVoid("message", msg);
_listeners?.invokeVoid("message", msg);
}
} }
catch(ex: Throwable) {} catch(ex: Throwable) {}
} }
} }
override fun closing(code: Int, reason: String) { override fun closing(code: Int, reason: String) {
if(hasClosing && _listeners?.isClosed != true) if(hasClosing)
{ {
try { try {
_package._plugin.busy { _listeners?.invokeVoid("closing", code, reason);
_listeners?.invokeVoid("closing", code, reason);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
@ -701,29 +623,21 @@ class PackageHttp: V8Package {
} }
override fun closed(code: Int, reason: String) { override fun closed(code: Int, reason: String) {
_isOpen = false; _isOpen = false;
if(hasClosed && _listeners?.isClosed != true) { if(hasClosed) {
try { try {
_package._plugin.busy { _listeners?.invokeVoid("closed", code, reason);
_listeners?.invokeVoid("closed", code, reason);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
} }
} }
Logger.w(TAG, "PackageHttp Socket removed");
synchronized(_package.aliveSockets) {
_package.aliveSockets.remove(this@SocketResult);
}
} }
override fun failure(exception: Throwable) { override fun failure(exception: Throwable) {
_isOpen = false; _isOpen = false;
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception); Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
if(hasFailure && _listeners?.isClosed != true) { if(hasFailure) {
try { try {
_package._plugin.busy { _listeners?.invokeVoid("failure", exception.message);
_listeners?.invokeVoid("failure", exception.message);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);

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