[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-93306":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":14,"stars7d":14,"stars30d":14,"stars90d":14,"forks30d":14,"starsTrendScore":14,"compositeScore":15,"rankGlobal":9,"rankLanguage":9,"license":16,"archived":17,"fork":17,"defaultBranch":18,"hasWiki":19,"hasPages":17,"topics":20,"createdAt":9,"pushedAt":9,"updatedAt":21,"readmeContent":22,"aiSummary":23,"trendingCount":14,"starSnapshotCount":14,"syncStatus":24,"lastSyncTime":25,"discoverSource":26},93306,"vmodal_sdk_android","v-modal\u002Fvmodal_sdk_android","v-modal","Multimodal Video Search SDK for Android Kotlin",null,"Kotlin",157,21,119,0,44.03,"Apache License 2.0",false,"main",true,[],"2026-07-22 04:02:08","> ### Get a free beta API key\n>\n> Join the V-Modal beta by [filling out the contact form](https:\u002F\u002Fv-modal.com\u002Fpage\u002Fcontact.ts).\n\n> ### Active support in Discord\n>\n> We would love to help you build with V-Modal—come say hello in our [Discord channel](https:\u002F\u002Fdiscord.gg\u002FCRNsdJHg6)!\n\n\u003Cdiv align=\"center\">\n\n\u003Cimg src=\"assets\u002Fvmodal-banner.svg\" alt=\"V-Modal Android SDK\" width=\"760\"\u002F>\n\n\u003Cbr\u002F>\u003Cbr\u002F>\n\n**Search video with plain text from your Kotlin Android app.**\nNo video-processing stack, no vector database — just a client, an API key, and coroutines.\n\n[![Kotlin](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FKotlin-100%25-7F52FF?logo=kotlin&logoColor=white)](https:\u002F\u002Fkotlinlang.org)\n[![Android](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FAndroid-SDK-3DDC84?logo=android&logoColor=white)](https:\u002F\u002Fdeveloper.android.com)\n[![Java](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FJava-17-orange?logo=openjdk&logoColor=white)](https:\u002F\u002Fadoptium.net)\n[![Coroutines](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FCoroutines-friendly-5B6CFF)](https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fcoroutines-overview.html)\n[![Website](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fv--modal.com-visit-2EC5FF)](https:\u002F\u002Fv-modal.com)\n\n[Quick start](#-quick-start) •\n[Runtime API keys](#-runtime-api-key-contract) •\n[Search](#-search-a-collection) •\n[Upload](#-upload-a-video) •\n[Examples](examples\u002F01_starter\u002F) •\n[Search app](docs\u002Fsearch_app.md) •\n[API reference](DOC_REF.md) •\n[Troubleshooting](#-troubleshooting)\n\n\u003C\u002Fdiv>\n\n---\n\n## ✨ What you can do\n\n```kotlin\nval result = sdk.searches.searchVideo(\n    queryText = \"red car at night\",   \u002F\u002F describe the moment in plain words\n    groupName = \"traffic-cameras\",    \u002F\u002F your collection\n    streamName = \"astream\",\n    limit = 20,\n)\n```\n\nThat is the whole idea: upload videos into collections, then find moments in\nthem with natural language. The SDK also manages collections, uploads files\nwith signed streaming, offers explicit experimental multipart support for\ncapable gateways, and plays nicely with\n`Dispatchers.IO`, `lifecycleScope`, and WorkManager.\n\nRelease artifacts are configured to use the Maven Central coordinates\n`com.vmodal:vmodal-sdk-android:\u003Cversion>`. Source-project inclusion remains\navailable for contributors.\n\n## 📋 What you need\n\n| Requirement | Details |\n|---|---|\n| \u003Cimg src=\"assets\u002Fkotlin-original.svg\" width=\"16\"\u002F> Kotlin project | Android project using Gradle Kotlin DSL |\n| ☕ Java 17 | `sourceCompatibility` \u002F `jvmTarget = \"17\"` |\n| 📦 Maven Central | The requested version must have passed the release workflow and been published |\n| 🔑 API key | Loaded at runtime from your authenticated application backend |\n\n> ⚠️ Never bundle a real API key in source, `BuildConfig`, resources, or\n> `AndroidManifest.xml`. The parent application must inject it at runtime.\n\n## 🔒 Production security expectations\n\nUse the authenticated gateway with a user-scoped, revocable, short-lived\ncredential obtained from your application's authenticated backend. Provider\nmaster keys and token-minting secrets must never reach the APK. `mode =\n\"gateway\"` is the default, and gateway requests omit caller-supplied user,\ntenant, and email headers. Use `Client.unsafeDirect(...)` only for a trusted\nprivate network whose downstream service\nindependently authenticates and authorizes identity headers; it is unsafe as a\ncaller-trusted identity boundary on a public network.\n\nThe SDK automatically retries only `GET` and `HEAD`. `POST`, `PUT`, `PATCH`,\nand `DELETE` are sent once, even when a response is lost, unless the separate\nsigned multipart protocol can reconcile a part by status and ETag. Success\nbodies are bounded to 8 MiB for JSON\u002Ftext and 64 MiB for bytes; error bodies are\nbounded to 1 MiB. JSON and upload checkpoints are strictly parsed, and\nconfiguration\u002Ferror strings do not print tokens, URLs, identities, or response\nbodies. See the [upload and runtime contract](docs\u002Fsdk_doc.md) and [credential\nlifecycle](docs\u002Fmanage_api_key.md).\n\n## 🚀 Quick start\n\nThree steps from zero to your first API response.\n\n### 1️⃣ Add the SDK dependency\n\nConfirm that the requested SDK version is published in Maven Central. Until the\nfirst registry release completes, contributors must use the source project.\n\nMake sure your Android project's `settings.gradle.kts` contains Maven Central:\n\n```kotlin\ndependencyResolutionManagement {\n    repositories {\n        mavenCentral()\n    }\n}\n```\n\n### 2️⃣ Configure your app module\n\nIn the app module's `build.gradle.kts`, use Java 17 and add the SDK dependency:\n\n```kotlin\nandroid {\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n    kotlinOptions {\n        jvmTarget = \"17\"\n    }\n}\n\ndependencies {\n    implementation(\"com.vmodal:vmodal-sdk-android:1.0.0\")\n}\n```\n\nSync the Gradle project, then allow network access in\n`app\u002Fsrc\u002Fmain\u002FAndroidManifest.xml` (directly inside `\u003Cmanifest>`):\n\n```xml\n\u003Cuses-permission android:name=\"android.permission.INTERNET\" \u002F>\n```\n\n### 3️⃣ Connect and print the API status\n\nV-Modal calls perform network I\u002FO. Run them from `Dispatchers.IO`, WorkManager,\nor another worker thread — never the Android main thread.\n\nThe following function authenticates the API key, creates the ready-to-use\nclient, and returns the first visible result:\n\n```kotlin\nimport com.vmodal.sdk.Client\nimport com.vmodal.sdk.MutableApiKeyProvider\nimport com.vmodal.sdk.PUBLIC_GATEWAY_URL\nimport com.vmodal.sdk.SdkConfig\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nsuspend fun checkVmodal(apiKeys: MutableApiKeyProvider): Client = withContext(Dispatchers.IO) {\n    val firstClient = Client(\n        SdkConfig(\n            baseUrl = PUBLIC_GATEWAY_URL,\n            userId = \"\",\n            mode = \"gateway\",\n            apiKeyProvider = apiKeys,\n        )\n    )\n    val me = firstClient.auth.me()\n\n    val sdk = Client(\n        firstClient.cfg.copy(\n            userId = requireNotNull(me.userId),\n            tenantId = me.tenantId.orEmpty(),\n            email = me.email.orEmpty(),\n        )\n    )\n\n    val health = sdk.health()\n    println(\"VModal connected: ${health.status}\")\n    sdk\n}\n```\n\nCall it from an Activity or Fragment lifecycle scope:\n\n```kotlin\nimport androidx.lifecycle.lifecycleScope\nimport kotlinx.coroutines.launch\n\nlifecycleScope.launch {\n    val apiKeys = MutableApiKeyProvider(apiKeyLoadedByYourApp)\n    val sdk = checkVmodal(apiKeys)\n    \u002F\u002F Keep or pass sdk to the code that needs V-Modal.\n    \u002F\u002F Retain apiKeys at application scope, then call apiKeys.rotate(freshKey).\n    \u002F\u002F On logout\u002Faccount switch: clear persisted state, then apiKeys.clear().\n}\n```\n\nUse `viewModelScope.launch { ... }` instead when the client belongs to a\nViewModel. A printed `VModal connected: ...` message means installation,\nauthentication, and network access are all working. 🎉\n\n### How the connection works\n\n```mermaid\nflowchart LR\n    A[\"🔑 Runtime API key\"] --> B[\"MutableApiKeyProvider\"]\n    B --> C[\"Client(mode = gateway)\"]\n    C --> D[\"auth.me()\"]\n    D --> E[\"Client(cfg + userId)\"]\n    E --> F[\"health() \u002F search \u002F upload\"]\n```\n\n## 🔄 Runtime API-key contract\n\nThe parent Android application and this SDK have separate responsibilities.\nThe app owns the credential lifecycle; the SDK only reads the injected current\nvalue while building an authenticated request.\n\n| Parent application owns | SDK owns |\n|---|---|\n| Authenticate the signed-in app user | `ApiKeyProvider` request-time contract |\n| Fetch the API key from the app backend | Atomic swaps and fail-closed clearing in `MutableApiKeyProvider` |\n| Choose secure, app-owned persistence | One key snapshot per authenticated request |\n| Refresh, version, and serialize rotations | `Authorization: Bearer \u003Ckey>` on existing authenticated routes |\n| Decide whether a failed operation is safe to retry | Filtering auth headers from presigned R2 upload requests |\n\n`ApiKeyProvider.current()` is synchronous. It must return an already-loaded\nvalue and must not perform storage or network I\u002FO. Keep the provider and\n`Client` at application scope so Activities, coroutines, and WorkManager jobs\nall observe the same rotations.\n\n```mermaid\nflowchart LR\n    subgraph App[\"Parent Android application\"]\n        A[\"App-owned secure storage\"] --> B[\"Load cached key on worker thread\"]\n        C[\"Authenticated app backend\"] --> D[\"Fetch latest key\"]\n        B --> E[\"MutableApiKeyProvider\"]\n        D -->|\"persist, then rotate(newKey)\"| E\n    end\n    subgraph SDK[\"uinterface\u002Fsdk_android\"]\n        F[\"SdkConfig(apiKeyProvider)\"] --> G[\"Client\"]\n        G --> H[\"Build authenticated request\"]\n        H --> I[\"Authorization: Bearer key snapshot\"]\n    end\n    E --> F\n```\n\nAt application startup:\n\n1. On `Dispatchers.IO`, load the signed-in session and cached API key from\n   app-owned storage. If no key exists, complete sign-in and fetch the first key\n   before creating an authenticated client.\n2. Create one `MutableApiKeyProvider`, inject it through `SdkConfig`, call\n   `auth.me()`, and install the resolved `Client` in the app dependency graph.\n3. Fetch a newer key in the background. Validate and persist it using the\n   app's policy, then call `rotate(newKey)`.\n\nRotation changes the next request; a request already created keeps its original\nheader. A blank initial or rotated key raises `ValidationFailed`, and a failed\nrotation leaves the last working key active. Rotation is only valid for another\nkey belonging to the same V-Modal identity. For a different user or tenant,\ncreate a new `Client` and resolve `auth.me()` again.\n\nOn logout or account switch, first stop\u002Fcancel work that uses the client, clear\nthe app's persisted credential, and call `apiKeys.clear()` (or `close()`). The\noperation is idempotent. Later authenticated requests fail closed with\n`AuthError`; a configured provider never falls back to the legacy static token.\nClearing removes the SDK's live reference, but immutable JVM strings cannot be\nguaranteed to be zeroized from every old heap copy.\n\nAuthenticated API calls require HTTPS except for literal loopback development\nhosts, reject cross-origin absolute URLs, and do not follow redirects. Signed\nuploads apply the same HTTPS policy, strip API identity headers, and do not\nfollow redirects.\n\nOn `401`, the app may refresh the key and retry one safe, idempotent operation\nonce. Do not treat `403` as proof of expiration, and do not automatically replay\nuploads or mutating `POST` requests. See the framework-free\n[rotation example](examples\u002F01_starter\u002F03_rotate_api_key.kt).\n\nThe legacy `token = \"...\"` constructors and `SdkConfig.fromEnv()` remain\nsupported for JVM tools, CI, and existing integrations. `apiKeyProvider` takes\nprecedence when both are supplied.\n\n## 📁 List your collections\n\nOnce the quick start works, use the returned `sdk` client on the same worker\ncontext:\n\n```kotlin\nval groups = sdk.collections.listGroups(mode = \"vid_file\")\nprintln(\"Collections: ${groups.total}\")\ngroups.data.forEach(::println)\n```\n\nThis is a useful second check because it confirms that the authenticated user\ncan reach their V-Modal data.\n\n## 🔍 Search a collection\n\nReplace `traffic-cameras` with a collection returned by `listGroups()`:\n\n```kotlin\nval result = sdk.searches.searchVideo(\n    queryText = \"red car at night\",\n    groupName = \"traffic-cameras\",\n    streamName = \"astream\",\n    limit = 20,\n)\n\nprintln(\"Matches returned: ${result.cntActual}\")\nresult.data.forEach(::println)\n```\n\n> 💡 If the call succeeds but returns no matches, first confirm the collection\n> name, stream name, and query text. An empty result is different from an API\n> error.\n\n## 📤 Upload a video\n\nAfter authentication and search work, continue with the upload examples. The\nAndroid-safe path is:\n\n```mermaid\nflowchart LR\n    A[\"🎬 User picks video\u003Cbr\u002F>(content:\u002F\u002F URI)\"] --> B[\"UploadSource\u003Cbr\u002F>example 08\"]\n    B --> C[\"videoUploadAsync()\u003Cbr\u002F>example 09\"]\n    C --> D[\"UploadHandle\u003Cbr\u002F>(progress \u002F cancel)\"]\n```\n\n1. Let the user select a video and obtain a `content:\u002F\u002F` URI.\n2. Convert the URI to an `UploadSource` with\n   [example 08](examples\u002F01_starter\u002F08_content_uri_source.kt).\n3. Start the upload with\n   [example 09](examples\u002F01_starter\u002F09_async_video_upload.kt).\n4. Keep the returned `UploadHandle` if the UI needs a Cancel action.\n\nThe SDK streams the video instead of loading the whole file into memory. Every\nfile size uses the supported single signed-URL flow by default. Multipart is an\nexperimental explicit opt-in with `VideoUploadOptions(multipart = true)` for a\ngateway that exposes the complete multipart route family. A gateway without\nthat capability fails with `FeatureDisabled` instead of silently selecting a\nmissing API by file size.\n\n## 🛠️ Troubleshooting\n\n| Symptom | Fix |\n|---|---|\n| `VMODAL_API_KEY is required` | `Client.fromEnv()` is intended for JVM tools and CI, where environment variables exist. In an Android app, inject a runtime API-key provider as shown in the quick start. |\n| `auth\u002Fme returned no user_id` or auth error | Confirm the API key is current and belongs to the environment identified by `PUBLIC_GATEWAY_URL`. Do not invent or hard-code a user ID; `auth.me()` resolves the key owner. |\n| `NetworkOnMainThreadException` or frozen UI | Move blocking calls (`auth.me()`, `health()`, `listGroups()`, `searchVideo()`) to `Dispatchers.IO` or WorkManager. `videoUploadAsync()` already runs off the main thread, but its callbacks do too — switch to `Dispatchers.Main` before updating views. |\n| Gradle cannot resolve the SDK | Confirm `mavenCentral()` is configured and use a released version from the public repository. |\n\n## ✅ Verify the SDK checkout\n\nThese commands test the SDK itself; they are not required each time the Android\napp runs:\n\n```bash\ngradle --no-daemon clean build publishToMavenLocal\ncd examples\u002F02_search\n.\u002Fgradlew --no-daemon :app:assembleDebug \\\n  -PvmodalUseMavenLocal=true -PvmodalSdkVersion=1.0.0\n```\n\nThis verifies both the Maven publication and Android consumption. No emulator\nor API token is required. Maintainers can follow the\n[Maven Central release guide](docs\u002Fmaven_release.md).\n\n## 🗺️ Learn progressively\n\n| Step | Where | What you get |\n|---|---|---|\n| 1 | This page | Working client, first API response |\n| 2 | [Examples](examples\u002F01_starter\u002F) | Copy-paste building blocks, grouped by task |\n| 3 | [Upload guide](docs\u002Fsdk_doc.md) | Android URI uploads, cancellation, WorkManager, process-death resume |\n| 4 | [API quick reference](DOC_REF.md) | Every method and response type |\n\nAll typed response objects expose `raw: Map\u003CString, Any?>` for server fields\nthat do not yet have a typed property. All SDK failures derive from `SdkError`;\napplications can handle `AuthError`, `ValidationFailed`, `ApiError`, and\n`FeatureDisabled` separately when needed. Transport, size, and JSON failures are\navailable as `TransportError`, `ResponseTooLarge`, and `MalformedResponse`.\n\n---\n\n\u003Cdiv align=\"center\">\n\n\u003Cimg src=\"assets\u002Fkotlin-original.svg\" width=\"36\" alt=\"Kotlin\"\u002F>&nbsp;&nbsp;\n\u003Cimg src=\"assets\u002Fandroid-original.svg\" width=\"36\" alt=\"Android\"\u002F>&nbsp;&nbsp;\n\u003Cimg src=\"assets\u002Fandroidstudio-original.svg\" width=\"36\" alt=\"Android Studio\"\u002F>&nbsp;&nbsp;\n\u003Cimg src=\"assets\u002Fgradle-original.svg\" width=\"36\" alt=\"Gradle\"\u002F>\n\nBuilt for Kotlin developers, by [**v-modal.com**](https:\u002F\u002Fv-modal.com) 💜\n\n\u003Csub>Logo attributions in [assets\u002FREADME.md](assets\u002FREADME.md).\u003C\u002Fsub>\n\n\u003Csub>Licensed under the [Apache License 2.0](LICENSE).\u003C\u002Fsub>\n\n\u003C\u002Fdiv>\n","这是一个面向 Android 平台的多模态视频搜索 SDK，支持在 Kotlin 应用中通过自然语言描述（如“夜间行驶的红色汽车”）检索视频片段。核心功能包括视频上传、按文本查询视频时刻、集合管理及流式签名上传，底层依赖云端多模态理解服务，客户端无需自行部署视频处理流水线或向量数据库，仅需集成 SDK 并传入运行时 API 密钥。适用于移动端视频内容检索类场景，如安防监控回溯、教育视频片段定位、媒体资产管理等轻量级垂直需求。",2,"2026-07-16 02:30:02","CREATED_QUERY"]