@dvai-bridge/android-mediapipe-core 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +341 -34
- package/android/build.gradle +211 -134
- package/android/gradle.properties +1 -1
- package/android/settings.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +14 -14
- package/android/src/main/java/co/deepvoiceai/bridge/mediapipe/core/ImageDecoder.kt +115 -115
- package/android/src/main/java/co/deepvoiceai/bridge/mediapipe/core/MediaPipeBridge.kt +203 -203
- package/android/src/main/java/co/deepvoiceai/bridge/mediapipe/core/MediaPipeHandlers.kt +482 -482
- package/android/src/main/java/co/deepvoiceai/bridge/mediapipe/core/PluginState.kt +134 -134
- package/android/src/main/res/xml/dvai_network_security_config.xml +7 -7
- package/android/src/test/java/co/deepvoiceai/bridge/mediapipe/core/ImageDecoderTest.kt +114 -114
- package/android/src/test/java/co/deepvoiceai/bridge/mediapipe/core/MediaPipeHandlersTest.kt +529 -529
- package/android/src/test/java/co/deepvoiceai/bridge/mediapipe/core/PluginStateTest.kt +85 -85
- package/package.json +1 -1
- package/README.md +0 -199
package/android/build.gradle
CHANGED
|
@@ -1,134 +1,211 @@
|
|
|
1
|
-
buildscript {
|
|
2
|
-
ext {
|
|
3
|
-
kotlinVersion = '2.3.21'
|
|
4
|
-
}
|
|
5
|
-
repositories {
|
|
6
|
-
google()
|
|
7
|
-
mavenCentral()
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
classpath
|
|
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
|
-
implementation
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
implementation
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
implementation
|
|
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
|
-
|
|
1
|
+
buildscript {
|
|
2
|
+
ext {
|
|
3
|
+
kotlinVersion = '2.3.21'
|
|
4
|
+
}
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
mavenCentral()
|
|
8
|
+
gradlePluginPortal()
|
|
9
|
+
}
|
|
10
|
+
dependencies {
|
|
11
|
+
classpath 'com.android.tools.build:gradle:9.2.0'
|
|
12
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
|
13
|
+
// Maven Central publishing — wraps maven-publish + signing + the
|
|
14
|
+
// Central Portal upload API into one config block. See the
|
|
15
|
+
// mavenPublishing { ... } block below.
|
|
16
|
+
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.36.0'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ext {
|
|
21
|
+
junitVersion = '4.13.2'
|
|
22
|
+
androidxAppCompatVersion = '1.7.1'
|
|
23
|
+
coroutinesVersion = '1.10.2'
|
|
24
|
+
// Pinned within Ktor 2.x — Ktor 3 has breaking API changes around server
|
|
25
|
+
// configuration that would require non-trivial refactors. Stay on the
|
|
26
|
+
// latest 2.x patch (2.3.13) until a deliberate Ktor 3 migration.
|
|
27
|
+
ktorVersion = '2.3.13'
|
|
28
|
+
// LiteRT-LM — replaces the deprecated `com.google.mediapipe:tasks-genai`
|
|
29
|
+
// (Phase 3B, Tasks 18-19). `litertlm-android` bundles the full runtime;
|
|
30
|
+
// no separate `tasks-core` companion is required.
|
|
31
|
+
// Hosted on Google Maven (google() repo, already declared in allprojects).
|
|
32
|
+
litertLmVersion = '0.10.2'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
allprojects {
|
|
36
|
+
repositories {
|
|
37
|
+
google()
|
|
38
|
+
mavenCentral()
|
|
39
|
+
// Phase 3D: shared-core lives here in dev (after `publishToMavenLocal`).
|
|
40
|
+
// Production publishes resolve from GitHub Packages.
|
|
41
|
+
mavenLocal()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// AGP 9+ ships built-in Kotlin support; the standalone 'kotlin-android'
|
|
46
|
+
// plugin is no longer needed (and is rejected by the build).
|
|
47
|
+
// See https://kotl.in/gradle/agp-built-in-kotlin
|
|
48
|
+
apply plugin: 'com.android.library'
|
|
49
|
+
apply plugin: 'com.vanniktech.maven.publish'
|
|
50
|
+
|
|
51
|
+
android {
|
|
52
|
+
namespace 'co.deepvoiceai.bridge.mediapipe.core'
|
|
53
|
+
// Phase 2 Task 3 (examples) library fix: see shared-core/android/build.gradle
|
|
54
|
+
// for the rationale — `-PcompileSdkOverride=35` lets Windows hosts sidestep
|
|
55
|
+
// an AGP 9.2.0 parseLocalResources bug against android-36's public-final.xml.
|
|
56
|
+
compileSdk(project.findProperty('compileSdkOverride')?.toInteger() ?: 36)
|
|
57
|
+
|
|
58
|
+
defaultConfig {
|
|
59
|
+
minSdk 24
|
|
60
|
+
targetSdk(project.findProperty('compileSdkOverride')?.toInteger() ?: 36)
|
|
61
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
compileOptions {
|
|
65
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
66
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
67
|
+
}
|
|
68
|
+
testOptions {
|
|
69
|
+
unitTests.includeAndroidResources = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// NB: don't add `publishing { singleVariant('release') { withSourcesJar() } }`
|
|
73
|
+
// here. The vanniktech.maven.publish plugin auto-configures Android
|
|
74
|
+
// single-variant publication + sources + javadoc jars on its own; a
|
|
75
|
+
// manual singleVariant block collides with "Using singleVariant publishing
|
|
76
|
+
// DSL multiple times to publish variant 'release' to component 'release'
|
|
77
|
+
// is not allowed."
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// AGP 9 / Kotlin 2.x: the legacy android.kotlinOptions block is gone. The
|
|
81
|
+
// replacement is the standalone `kotlin { compilerOptions { ... } }` extension
|
|
82
|
+
// (provided by AGP's built-in Kotlin integration).
|
|
83
|
+
kotlin {
|
|
84
|
+
compilerOptions {
|
|
85
|
+
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
dependencies {
|
|
90
|
+
// Phase 3D: shared HTTP server + handler dispatch types live here.
|
|
91
|
+
// `api` (not `implementation`) so consumer projects see them transitively.
|
|
92
|
+
api "co.deepvoiceai:android-shared-core:$dvaiBridgeVersion"
|
|
93
|
+
|
|
94
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
95
|
+
implementation "io.ktor:ktor-server-core:$ktorVersion"
|
|
96
|
+
implementation "io.ktor:ktor-server-cio:$ktorVersion"
|
|
97
|
+
implementation "io.ktor:ktor-server-cors:$ktorVersion"
|
|
98
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
|
99
|
+
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0'
|
|
100
|
+
implementation 'com.squareup.okhttp3:okhttp:5.3.2'
|
|
101
|
+
|
|
102
|
+
// LiteRT-LM: non-deprecated successor to tasks-genai (Phase 3B Task 18-19).
|
|
103
|
+
// Bundles the full LLM runtime including vision support via EngineConfig.visionBackend.
|
|
104
|
+
// ~100MB native libs per ABI — first build will be slow.
|
|
105
|
+
implementation "com.google.ai.edge.litertlm:litertlm-android:$litertLmVersion"
|
|
106
|
+
|
|
107
|
+
testImplementation "junit:junit:$junitVersion"
|
|
108
|
+
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
|
109
|
+
testImplementation 'org.robolectric:robolectric:4.16.1'
|
|
110
|
+
testImplementation "io.ktor:ktor-server-test-host:$ktorVersion"
|
|
111
|
+
testImplementation 'com.squareup.okhttp3:mockwebserver:5.3.2'
|
|
112
|
+
|
|
113
|
+
androidTestImplementation 'androidx.test:runner:1.7.0'
|
|
114
|
+
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Maven Central publishing via Sonatype Central Portal. See the
|
|
118
|
+
// PUBLISHING-MAVEN-CENTRAL.md note at the repo root (gitignored — your
|
|
119
|
+
// operator playbook) for credentials + the per-release sequence.
|
|
120
|
+
//
|
|
121
|
+
// Coordinates and POM metadata live here; the actual publish is driven
|
|
122
|
+
// by `./gradlew publishAndReleaseToMavenCentral` (auto-release on
|
|
123
|
+
// validation passing) or `publishToMavenLocal` for a dry-run that
|
|
124
|
+
// writes the artifacts + signatures to ~/.m2/repository/.
|
|
125
|
+
mavenPublishing {
|
|
126
|
+
// AGP 9.2.0 bundles a Dokka build whose ASM can't read Kotlin 2.3.21's
|
|
127
|
+
// sealed-class bytecode (`PermittedSubclasses requires ASM9`) when it
|
|
128
|
+
// tries to resolve the api jar of android-shared-core during the
|
|
129
|
+
// `javaDocReleaseGeneration` task. We bypass AGP's Dokka javadoc and
|
|
130
|
+
// ship an empty javadoc.jar instead — Sonatype Central Portal validates
|
|
131
|
+
// the artifact's presence, not that it is populated. Re-enable once
|
|
132
|
+
// AGP upgrades its bundled ASM.
|
|
133
|
+
configure(new com.vanniktech.maven.publish.AndroidSingleVariantLibrary(
|
|
134
|
+
/* variant = */ 'release',
|
|
135
|
+
/* sourcesJar = */ true,
|
|
136
|
+
/* publishJavadoc = */ false,
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
publishToMavenCentral(true)
|
|
140
|
+
// Sign only when the PGP key is configured — see shared-core for rationale.
|
|
141
|
+
if (project.findProperty('signingInMemoryKey') ||
|
|
142
|
+
System.getenv('ORG_GRADLE_PROJECT_signingInMemoryKey')) {
|
|
143
|
+
signAllPublications()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
coordinates(
|
|
147
|
+
'co.deepvoiceai',
|
|
148
|
+
'android-mediapipe-core',
|
|
149
|
+
(project.findProperty('dvaiBridgeVersion') ?: '4.0.2').toString(),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
pom {
|
|
153
|
+
name = 'DVAI Bridge Android — MediaPipe / LiteRT-LM core'
|
|
154
|
+
description = 'LiteRT-LM (Google AI Edge) backend for DVAI Bridge on Android. ' +
|
|
155
|
+
'Replaces the deprecated MediaPipe tasks-genai runtime; bundles vision ' +
|
|
156
|
+
'support via EngineConfig.visionBackend. Depends on android-shared-core ' +
|
|
157
|
+
'for the HTTP/handler layer.'
|
|
158
|
+
inceptionYear = '2026'
|
|
159
|
+
url = 'https://github.com/dvai-global/dvai-bridge'
|
|
160
|
+
|
|
161
|
+
licenses {
|
|
162
|
+
license {
|
|
163
|
+
name = 'DVAI Bridge Community Licence v1.0'
|
|
164
|
+
url = 'https://bridge.deepvoiceai.co/licensing'
|
|
165
|
+
distribution = 'repo'
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
developers {
|
|
169
|
+
developer {
|
|
170
|
+
id = 'deepvoiceai'
|
|
171
|
+
name = 'Deep Voice AI Limited'
|
|
172
|
+
email = 'info@deepvoiceai.co'
|
|
173
|
+
organization = 'Deep Voice AI Limited'
|
|
174
|
+
organizationUrl = 'https://deepvoiceai.co'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
scm {
|
|
178
|
+
url = 'https://github.com/dvai-global/dvai-bridge'
|
|
179
|
+
connection = 'scm:git:git://github.com/dvai-global/dvai-bridge.git'
|
|
180
|
+
developerConnection = 'scm:git:ssh://github.com:dvai-global/dvai-bridge.git'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Switch Gradle signing from BouncyCastle-based in-memory mode to
|
|
186
|
+
// shell-out-to-gpg mode. The in-memory path can't parse GnuPG 2.4+
|
|
187
|
+
// keys that carry pref-aead-algos (subpkt 34) in their user-ID
|
|
188
|
+
// self-signature — `gpg --list-packets` of our v4.0.1 signing key
|
|
189
|
+
// confirms that subpacket is present. useGpgCmd() shells out to the
|
|
190
|
+
// local gpg binary (imported into the runner's keyring by the
|
|
191
|
+
// publish.yml workflow), which can parse it. Passphrase is supplied
|
|
192
|
+
// via ~/.gradle/gradle.properties (signing.gnupg.passphrase).
|
|
193
|
+
if (project.findProperty('signing.gnupg.keyName') ||
|
|
194
|
+
System.getenv('ORG_GRADLE_PROJECT_USE_GPG_CMD_SIGNING') == 'true') {
|
|
195
|
+
signing {
|
|
196
|
+
useGpgCmd()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Empty javadoc.jar to satisfy Sonatype Central Portal validation while
|
|
201
|
+
// AGP 9.2.0's bundled Dokka is incompatible with our sealed-class bytecode.
|
|
202
|
+
// See the `configure(AndroidSingleVariantLibrary ...)` comment above.
|
|
203
|
+
tasks.register('emptyJavadocJar', Jar) {
|
|
204
|
+
archiveClassifier.set('javadoc')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
afterEvaluate {
|
|
208
|
+
publishing.publications.withType(MavenPublication).configureEach { pub ->
|
|
209
|
+
pub.artifact(tasks.named('emptyJavadocJar'))
|
|
210
|
+
}
|
|
211
|
+
}
|
package/android/settings.gradle
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
rootProject.name = 'dvai-bridge-android-mediapipe-core'
|
|
1
|
+
rootProject.name = 'dvai-bridge-android-mediapipe-core'
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
-
<application>
|
|
4
|
-
<!-- LiteRT-LM GPU backend optional native libs (Phase 3B Task 18-19).
|
|
5
|
-
android:required="false" lets devices without these still run on
|
|
6
|
-
the CPU backend rather than failing installation outright. -->
|
|
7
|
-
<uses-native-library
|
|
8
|
-
android:name="libvndksupport.so"
|
|
9
|
-
android:required="false" />
|
|
10
|
-
<uses-native-library
|
|
11
|
-
android:name="libOpenCL.so"
|
|
12
|
-
android:required="false" />
|
|
13
|
-
</application>
|
|
14
|
-
</manifest>
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<application>
|
|
4
|
+
<!-- LiteRT-LM GPU backend optional native libs (Phase 3B Task 18-19).
|
|
5
|
+
android:required="false" lets devices without these still run on
|
|
6
|
+
the CPU backend rather than failing installation outright. -->
|
|
7
|
+
<uses-native-library
|
|
8
|
+
android:name="libvndksupport.so"
|
|
9
|
+
android:required="false" />
|
|
10
|
+
<uses-native-library
|
|
11
|
+
android:name="libOpenCL.so"
|
|
12
|
+
android:required="false" />
|
|
13
|
+
</application>
|
|
14
|
+
</manifest>
|
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
package co.deepvoiceai.bridge.mediapipe.core
|
|
2
|
-
|
|
3
|
-
import okhttp3.OkHttpClient
|
|
4
|
-
import okhttp3.Request
|
|
5
|
-
import java.io.File
|
|
6
|
-
import java.net.URI
|
|
7
|
-
import java.net.URLDecoder
|
|
8
|
-
import java.util.Base64
|
|
9
|
-
import java.util.concurrent.TimeUnit
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Errors thrown by [ImageDecoder.resolve] when the input URL string can't
|
|
13
|
-
* be turned into image bytes.
|
|
14
|
-
*/
|
|
15
|
-
sealed class ImageSourceError(msg: String) : Exception(msg) {
|
|
16
|
-
class MalformedDataURL(url: String) : ImageSourceError("malformed data URL: $url")
|
|
17
|
-
class MalformedURL(url: String) : ImageSourceError("malformed URL: $url")
|
|
18
|
-
class InvalidScheme(scheme: String) : ImageSourceError("unsupported URL scheme: $scheme")
|
|
19
|
-
class HttpError(val status: Int) : ImageSourceError("HTTP $status")
|
|
20
|
-
class Base64DecodeFailed : ImageSourceError("base64 decode failed")
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Resolves any of the three image URL schemes accepted by the DVAI bridge
|
|
25
|
-
* (`data:`, `https:`/`http:`, `file:`) into the raw encoded image bytes
|
|
26
|
-
* (PNG/JPEG/etc.). Format decoding into pixel buffers is performed downstream
|
|
27
|
-
* by Android's `BitmapFactory` (then wrapped in `MPImage` via
|
|
28
|
-
* `BitmapImageBuilder`) — this layer just materializes the encoded bytes.
|
|
29
|
-
*
|
|
30
|
-
* Byte-identical to capacitor-llama's `ImageDecoder` modulo package
|
|
31
|
-
* declaration. If you fix a bug here, mirror it there (and vice versa).
|
|
32
|
-
*
|
|
33
|
-
* Blocking: HTTP fetches use OkHttp synchronously. Call from a background
|
|
34
|
-
* thread or coroutine on `Dispatchers.IO` (the plugin layer already does).
|
|
35
|
-
*/
|
|
36
|
-
object ImageDecoder {
|
|
37
|
-
private val httpClient: OkHttpClient by lazy {
|
|
38
|
-
// `callTimeout` caps the entire HTTP exchange (DNS + connect + write
|
|
39
|
-
// + read + redirect) at 30s so worst-case latency matches iOS, where
|
|
40
|
-
// `URLRequest.timeoutInterval = 30` covers the whole request.
|
|
41
|
-
OkHttpClient.Builder()
|
|
42
|
-
.callTimeout(30, TimeUnit.SECONDS)
|
|
43
|
-
.build()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Resolve any supported URL scheme into raw image bytes.
|
|
48
|
-
*
|
|
49
|
-
* - `data:` URLs are parsed for an optional `;base64` token and decoded
|
|
50
|
-
* accordingly (URL-encoded payloads are also supported).
|
|
51
|
-
* - `https:` / `http:` URLs are fetched via OkHttp with a 30s timeout;
|
|
52
|
-
* non-2xx responses throw [ImageSourceError.HttpError].
|
|
53
|
-
* - `file:` URLs are read off disk via [File.readBytes].
|
|
54
|
-
* - Any other scheme throws [ImageSourceError.InvalidScheme].
|
|
55
|
-
* - URLs that fail to parse, lack a scheme, or have a missing/empty
|
|
56
|
-
* `file:` path throw [ImageSourceError.MalformedURL].
|
|
57
|
-
*/
|
|
58
|
-
fun resolve(url: String): ByteArray {
|
|
59
|
-
if (url.startsWith("data:")) {
|
|
60
|
-
return resolveDataURL(url)
|
|
61
|
-
}
|
|
62
|
-
return resolveWithClient(url, httpClient)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Test seam: same contract as [resolve] but lets the caller inject an
|
|
67
|
-
* [OkHttpClient] (e.g. one pointed at MockWebServer). Data URLs still
|
|
68
|
-
* short-circuit before the client is consulted.
|
|
69
|
-
*/
|
|
70
|
-
internal fun resolveWithClient(url: String, client: OkHttpClient): ByteArray {
|
|
71
|
-
if (url.startsWith("data:")) {
|
|
72
|
-
return resolveDataURL(url)
|
|
73
|
-
}
|
|
74
|
-
val parsed = try {
|
|
75
|
-
URI(url)
|
|
76
|
-
} catch (_: Exception) {
|
|
77
|
-
throw ImageSourceError.MalformedURL(url)
|
|
78
|
-
}
|
|
79
|
-
val scheme = parsed.scheme?.lowercase() ?: throw ImageSourceError.MalformedURL(url)
|
|
80
|
-
return when (scheme) {
|
|
81
|
-
"https", "http" -> fetchHttp(url, client)
|
|
82
|
-
"file" -> {
|
|
83
|
-
val path = parsed.path
|
|
84
|
-
if (path.isNullOrEmpty()) throw ImageSourceError.MalformedURL(url)
|
|
85
|
-
File(path).readBytes()
|
|
86
|
-
}
|
|
87
|
-
else -> throw ImageSourceError.InvalidScheme(scheme)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Parse a `data:[<mediatype>][;base64],<payload>` URL into raw bytes. */
|
|
92
|
-
private fun resolveDataURL(url: String): ByteArray {
|
|
93
|
-
val commaIdx = url.indexOf(',')
|
|
94
|
-
if (commaIdx < 0) throw ImageSourceError.MalformedDataURL(url)
|
|
95
|
-
// Skip the leading "data:" (5 chars) and isolate header / body.
|
|
96
|
-
val header = url.substring(5, commaIdx)
|
|
97
|
-
val body = url.substring(commaIdx + 1)
|
|
98
|
-
if (header.contains(";base64")) {
|
|
99
|
-
return try {
|
|
100
|
-
Base64.getDecoder().decode(body)
|
|
101
|
-
} catch (_: IllegalArgumentException) {
|
|
102
|
-
throw ImageSourceError.Base64DecodeFailed()
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Non-base64: payload is percent-encoded text per RFC 2397.
|
|
106
|
-
return URLDecoder.decode(body, Charsets.UTF_8.name()).toByteArray(Charsets.UTF_8)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private fun fetchHttp(url: String, client: OkHttpClient): ByteArray {
|
|
110
|
-
client.newCall(Request.Builder().url(url).build()).execute().use { resp ->
|
|
111
|
-
if (!resp.isSuccessful) throw ImageSourceError.HttpError(resp.code)
|
|
112
|
-
return resp.body?.bytes() ?: ByteArray(0)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
1
|
+
package co.deepvoiceai.bridge.mediapipe.core
|
|
2
|
+
|
|
3
|
+
import okhttp3.OkHttpClient
|
|
4
|
+
import okhttp3.Request
|
|
5
|
+
import java.io.File
|
|
6
|
+
import java.net.URI
|
|
7
|
+
import java.net.URLDecoder
|
|
8
|
+
import java.util.Base64
|
|
9
|
+
import java.util.concurrent.TimeUnit
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Errors thrown by [ImageDecoder.resolve] when the input URL string can't
|
|
13
|
+
* be turned into image bytes.
|
|
14
|
+
*/
|
|
15
|
+
sealed class ImageSourceError(msg: String) : Exception(msg) {
|
|
16
|
+
class MalformedDataURL(url: String) : ImageSourceError("malformed data URL: $url")
|
|
17
|
+
class MalformedURL(url: String) : ImageSourceError("malformed URL: $url")
|
|
18
|
+
class InvalidScheme(scheme: String) : ImageSourceError("unsupported URL scheme: $scheme")
|
|
19
|
+
class HttpError(val status: Int) : ImageSourceError("HTTP $status")
|
|
20
|
+
class Base64DecodeFailed : ImageSourceError("base64 decode failed")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolves any of the three image URL schemes accepted by the DVAI bridge
|
|
25
|
+
* (`data:`, `https:`/`http:`, `file:`) into the raw encoded image bytes
|
|
26
|
+
* (PNG/JPEG/etc.). Format decoding into pixel buffers is performed downstream
|
|
27
|
+
* by Android's `BitmapFactory` (then wrapped in `MPImage` via
|
|
28
|
+
* `BitmapImageBuilder`) — this layer just materializes the encoded bytes.
|
|
29
|
+
*
|
|
30
|
+
* Byte-identical to capacitor-llama's `ImageDecoder` modulo package
|
|
31
|
+
* declaration. If you fix a bug here, mirror it there (and vice versa).
|
|
32
|
+
*
|
|
33
|
+
* Blocking: HTTP fetches use OkHttp synchronously. Call from a background
|
|
34
|
+
* thread or coroutine on `Dispatchers.IO` (the plugin layer already does).
|
|
35
|
+
*/
|
|
36
|
+
object ImageDecoder {
|
|
37
|
+
private val httpClient: OkHttpClient by lazy {
|
|
38
|
+
// `callTimeout` caps the entire HTTP exchange (DNS + connect + write
|
|
39
|
+
// + read + redirect) at 30s so worst-case latency matches iOS, where
|
|
40
|
+
// `URLRequest.timeoutInterval = 30` covers the whole request.
|
|
41
|
+
OkHttpClient.Builder()
|
|
42
|
+
.callTimeout(30, TimeUnit.SECONDS)
|
|
43
|
+
.build()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve any supported URL scheme into raw image bytes.
|
|
48
|
+
*
|
|
49
|
+
* - `data:` URLs are parsed for an optional `;base64` token and decoded
|
|
50
|
+
* accordingly (URL-encoded payloads are also supported).
|
|
51
|
+
* - `https:` / `http:` URLs are fetched via OkHttp with a 30s timeout;
|
|
52
|
+
* non-2xx responses throw [ImageSourceError.HttpError].
|
|
53
|
+
* - `file:` URLs are read off disk via [File.readBytes].
|
|
54
|
+
* - Any other scheme throws [ImageSourceError.InvalidScheme].
|
|
55
|
+
* - URLs that fail to parse, lack a scheme, or have a missing/empty
|
|
56
|
+
* `file:` path throw [ImageSourceError.MalformedURL].
|
|
57
|
+
*/
|
|
58
|
+
fun resolve(url: String): ByteArray {
|
|
59
|
+
if (url.startsWith("data:")) {
|
|
60
|
+
return resolveDataURL(url)
|
|
61
|
+
}
|
|
62
|
+
return resolveWithClient(url, httpClient)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Test seam: same contract as [resolve] but lets the caller inject an
|
|
67
|
+
* [OkHttpClient] (e.g. one pointed at MockWebServer). Data URLs still
|
|
68
|
+
* short-circuit before the client is consulted.
|
|
69
|
+
*/
|
|
70
|
+
internal fun resolveWithClient(url: String, client: OkHttpClient): ByteArray {
|
|
71
|
+
if (url.startsWith("data:")) {
|
|
72
|
+
return resolveDataURL(url)
|
|
73
|
+
}
|
|
74
|
+
val parsed = try {
|
|
75
|
+
URI(url)
|
|
76
|
+
} catch (_: Exception) {
|
|
77
|
+
throw ImageSourceError.MalformedURL(url)
|
|
78
|
+
}
|
|
79
|
+
val scheme = parsed.scheme?.lowercase() ?: throw ImageSourceError.MalformedURL(url)
|
|
80
|
+
return when (scheme) {
|
|
81
|
+
"https", "http" -> fetchHttp(url, client)
|
|
82
|
+
"file" -> {
|
|
83
|
+
val path = parsed.path
|
|
84
|
+
if (path.isNullOrEmpty()) throw ImageSourceError.MalformedURL(url)
|
|
85
|
+
File(path).readBytes()
|
|
86
|
+
}
|
|
87
|
+
else -> throw ImageSourceError.InvalidScheme(scheme)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Parse a `data:[<mediatype>][;base64],<payload>` URL into raw bytes. */
|
|
92
|
+
private fun resolveDataURL(url: String): ByteArray {
|
|
93
|
+
val commaIdx = url.indexOf(',')
|
|
94
|
+
if (commaIdx < 0) throw ImageSourceError.MalformedDataURL(url)
|
|
95
|
+
// Skip the leading "data:" (5 chars) and isolate header / body.
|
|
96
|
+
val header = url.substring(5, commaIdx)
|
|
97
|
+
val body = url.substring(commaIdx + 1)
|
|
98
|
+
if (header.contains(";base64")) {
|
|
99
|
+
return try {
|
|
100
|
+
Base64.getDecoder().decode(body)
|
|
101
|
+
} catch (_: IllegalArgumentException) {
|
|
102
|
+
throw ImageSourceError.Base64DecodeFailed()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Non-base64: payload is percent-encoded text per RFC 2397.
|
|
106
|
+
return URLDecoder.decode(body, Charsets.UTF_8.name()).toByteArray(Charsets.UTF_8)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun fetchHttp(url: String, client: OkHttpClient): ByteArray {
|
|
110
|
+
client.newCall(Request.Builder().url(url).build()).execute().use { resp ->
|
|
111
|
+
if (!resp.isSuccessful) throw ImageSourceError.HttpError(resp.code)
|
|
112
|
+
return resp.body?.bytes() ?: ByteArray(0)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|