-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathHybridRiveFileFactory.kt
More file actions
179 lines (157 loc) · 5.68 KB
/
HybridRiveFileFactory.kt
File metadata and controls
179 lines (157 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package com.margelo.nitro.rive
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Choreographer
import androidx.annotation.Keep
import app.rive.RiveFile
import app.rive.RiveFileSource
import app.rive.core.CommandQueue
import com.facebook.proguard.annotations.DoNotStrip
import com.margelo.nitro.core.ArrayBuffer
import com.margelo.nitro.core.Promise
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Custom RiveLog logger that routes all Rive C++ runtime logs through [RiveLog]
* and broadcasts error messages to registered listeners. This captures C++ errors
* from the Rive CommandQueue (e.g., "State machine not found", "Draw failed").
*/
object RiveErrorLogger : app.rive.RiveLog.Logger {
private val listeners = mutableListOf<(String) -> Unit>()
private val reportedErrors = mutableSetOf<String>()
fun addListener(listener: (String) -> Unit) {
synchronized(listeners) { listeners.add(listener) }
}
fun removeListener(listener: (String) -> Unit) {
synchronized(listeners) { listeners.remove(listener) }
}
private fun broadcastError(tag: String, msg: String) {
val key = "$tag:$msg"
synchronized(reportedErrors) {
if (!reportedErrors.add(key)) return
}
synchronized(listeners) {
listeners.toList().forEach { it("[$tag] $msg") }
}
}
fun resetReportedErrors() {
synchronized(reportedErrors) { reportedErrors.clear() }
}
override fun v(tag: String, msg: () -> String) {
RiveLog.d(tag, msg())
}
override fun d(tag: String, msg: () -> String) {
RiveLog.d(tag, msg())
}
override fun i(tag: String, msg: () -> String) {
RiveLog.i(tag, msg())
}
override fun w(tag: String, msg: () -> String) {
RiveLog.w(tag, msg())
}
override fun e(tag: String, t: Throwable?, msg: () -> String) {
val message = msg()
RiveLog.e(tag, message)
broadcastError(tag, message)
}
}
@Keep
@DoNotStrip
class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
override val backend: String = "experimental"
companion object {
private const val TAG = "HybridRiveFileFactory"
@Volatile
private var sharedWorker: CommandQueue? = null
private var pollingStarted = false
@Synchronized
fun getSharedWorker(): CommandQueue {
if (app.rive.RiveLog.logger !is RiveErrorLogger) {
app.rive.RiveLog.logger = RiveErrorLogger
Log.d(TAG, "RiveErrorLogger installed")
}
return sharedWorker ?: CommandQueue().also {
sharedWorker = it
Log.d(TAG, "Created CommandQueue, refCount=${it.refCount}")
startPolling(it)
}
}
/**
* The experimental Rive SDK's CommandQueue needs to be polled every frame
* to process responses from the C++ command server. Without polling,
* all suspend functions (like RiveFile.fromSource) hang indefinitely.
*/
private fun startPolling(worker: CommandQueue) {
if (pollingStarted) return
pollingStarted = true
Handler(Looper.getMainLooper()).post {
val callback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
try {
worker.pollMessages()
} catch (e: Exception) {
Log.e(TAG, "pollMessages error", e)
}
Choreographer.getInstance().postFrameCallback(this)
}
}
Choreographer.getInstance().postFrameCallback(callback)
}
}
}
private suspend fun buildRiveFile(
data: ByteArray,
referencedAssets: ReferencedAssetsType?
): HybridRiveFile {
val worker = getSharedWorker()
ExperimentalAssetLoader.registerAssets(referencedAssets, worker)
val source = RiveFileSource.Bytes(data)
val result = RiveFile.fromSource(source, worker)
val riveFile = when (result) {
is app.rive.Result.Success -> result.value
is app.rive.Result.Error -> throw RuntimeException("Failed to load Rive file: ${result.throwable.message}", result.throwable)
else -> throw RuntimeException("Failed to load Rive file: unexpected result")
}
return HybridRiveFile(riveFile, worker)
}
override fun fromURL(url: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
return Promise.async {
val data = withContext(Dispatchers.IO) {
HTTPDataLoader.downloadBytes(url)
}
buildRiveFile(data, referencedAssets)
}
}
override fun fromFileURL(fileURL: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
if (!fileURL.startsWith("file://")) {
throw IllegalArgumentException("fromFileURL: URL must be a file URL: $fileURL")
}
return Promise.async {
val uri = java.net.URI(fileURL)
val path = uri.path ?: throw IllegalArgumentException("fromFileURL: Invalid URL: $fileURL")
val data = withContext(Dispatchers.IO) {
FileDataLoader.loadBytes(path)
}
buildRiveFile(data, referencedAssets)
}
}
@SuppressLint("DiscouragedApi")
override fun fromResource(resource: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
return Promise.async {
val data = withContext(Dispatchers.IO) {
ResourceDataLoader.loadBytes(resource)
}
buildRiveFile(data, referencedAssets)
}
}
override fun fromBytes(bytes: ArrayBuffer, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
val buffer = bytes.getBuffer(false)
return Promise.async {
val byteArray = ByteArray(buffer.remaining())
buffer.get(byteArray)
buildRiveFile(byteArray, referencedAssets)
}
}
}