# Nosmai Moderation - Full Documentation > On-device content and text moderation SDK for mobile apps. It flags NSFW imagery, detects weapons, drugs, cigarettes and alcohol, and moderates toxic text. Everything runs fully offline on the device, so no frame or message ever leaves the phone. Thresholds are tunable at runtime. Available for iOS, Android and Flutter. This file is the complete Nosmai Moderation documentation concatenated into one document for AI assistants. Each section lists its canonical page URL. For the curated link index, see llms.txt. Supported platforms: iOS (Swift) via CocoaPods `NosmaiModerationSDK`, Android (Kotlin) via `nosmai-detection.aar`, Flutter via `nosmai_moderation_sdk`. The license key looks like `NOSMAI-XXXX`, is bound to one app and platform, and is verified online on first launch then cached for 24h with a further 24h offline grace. --- # Introduction Source: https://nosmai.com/docs/moderation/introduction ## What is Nosmai Moderation? Nosmai Moderation is a content safety SDK that runs entirely on the user's device. It checks images, recorded video, chat text and the live camera for unsafe content, and returns a clear verdict your app can act on. There is no cloud round-trip, so frames and messages never leave the phone. You install one SDK, initialize it with a license key, and call a small set of methods. All models ship inside the SDK, so checks keep working offline after the one-time setup. ## What it moderates | Surface | What it does | | --- | --- | | Image | Check a single photo | | Video | Sample and aggregate a recorded clip | | Text | Check a chat message or comment | | Live camera | Real-time, per-frame moderation | It detects unsafe objects (weapon, drug, cigarette, alcohol), adult / NSFW imagery, and unsafe text (profanity, toxicity, hate, harassment, threats). ## Why on-device - Privacy: user content never leaves the device, which helps with compliance. - Latency: no network call per check, so results are immediate. - Cost: no per-request cloud bill for moderation. - Offline: works without connectivity after the one-time license check. ## Supported platforms | Platform | Package | Minimum version | | --- | --- | --- | | iOS (Swift) | `NosmaiModerationSDK` (CocoaPods) | iOS 15.1+ | | Android (Kotlin) | `nosmai-detection.aar` | API 24+ (arm64-v8a), Kotlin 2.2+ | | Flutter | `nosmai_moderation_sdk` | Flutter 3+ | | React Native | `@nosmai/moderation-react-native` | New Architecture (TurboModules) | New here? The Quickstart takes you from install to your first verdict in a few minutes. --- # Quickstart Source: https://nosmai.com/docs/moderation/quickstart ## Before you start You need a Nosmai license key for your app. It looks like `NOSMAI-XXXX` and is tied to your app's package name or bundle identifier and to the platform. Get one from the Nosmai dashboard. The first launch verifies the key online, so make sure the device has connectivity the first time you run your app. ## 1. Install Pick your platform. Flutter: ```yaml dependencies: nosmai_moderation_sdk: ^1.0.0 ``` iOS: ```ruby pod 'NosmaiModerationSDK', '~> 1.0' ``` Android (download `nosmai-detection.aar` from the releases page, put it in `app/libs/`, then reference it): ```kotlin dependencies { implementation(files("libs/nosmai-detection.aar")) } ``` ## 2. Initialize Initialize once at startup with your license key. Initialization does a network check and loads the models, so run it off the main thread. Flutter: ```dart final res = await NosmaiModeration.initialize('NOSMAI-XXXX'); if (!res.success) debugPrint('init failed: ${res.error}'); ``` iOS: ```swift DispatchQueue.global(qos: .userInitiated).async { let ok = NosmaiSDK.initialize(licenseKey: "NOSMAI-XXXX") } ``` Android: ```kotlin Executors.newSingleThreadExecutor().execute { val ok = NosmaiSDK.init(context, "NOSMAI-XXXX") } ``` ## 3. Run your first check Moderate an image and read the verdict. Flutter: ```dart final r = await NosmaiModeration.analyzeImage(file.path); print(r.isUnsafe ? 'UNSAFE' : 'SAFE'); ``` iOS: ```swift if let r = NosmaiSDK.analyzeImage(uiImage) { print(r.isUnsafe ? "UNSAFE" : "SAFE") } ``` Android: ```kotlin val r = NosmaiSDK.analyzeImage(bitmap) Log.d("Mod", if (r.isUnsafe) "UNSAFE" else "SAFE") ``` ## Next steps - How moderation works: surfaces, detection types, results and thresholds. - Platform guides: iOS, Android, Flutter. - Authentication: how license keys and offline validation work. --- # Authentication Source: https://nosmai.com/docs/moderation/authentication ## License keys The SDK authenticates with a license key that looks like `NOSMAI-XXXX`. Each key is issued for one app and is bound to: - your app's package name (Android) or bundle identifier (iOS), and - the platform (iOS or Android). A key used from a different app or platform is rejected. Get your key from the Nosmai dashboard and pass it to `init` / `initialize` at startup. The license key is an app-level identifier, not a server secret. It does not unlock anything on its own: the SDK verifies it with the Nosmai licensing service before any models load. ## How verification works 1. On the first launch, the SDK verifies the key online. The device must have connectivity this one time. 2. After a successful check, the result is cached on the device for 24 hours, so later launches do not need the network. 3. If the device is offline when the cache expires, the SDK keeps working for a further 24 hours of grace (48 hours total) before it must reconnect. If the key is missing, invalid, or expired, initialization fails and the SDK does not run. Handle the failure return from `init` / `initialize` and do not moderate until it succeeds. Because the first launch needs connectivity and verification loads the models, always call init off the main thread. ## What your license includes A license enables a specific set of capabilities. The SDK only runs what your plan includes. Moderation types: | Capability | Method | | --- | --- | | Image moderation | `analyzeImage` | | Video moderation | `analyzeVideo` | | Text moderation | `moderateText` | | Live moderation | `startStream` and live frames | Detection categories: `weapon`, `drug`, `cigarette`, `alcohol`, and `adult` (NSFW). If a moderation type is not in your plan, that call does nothing and returns a safe or empty result. If a detection category is not in your plan, it is never reported. Contact Nosmai to change what your license includes. ## Going to production Use your production license key in release builds, and keep test and production keys separate so test traffic does not count against production usage. --- # How moderation works (Concepts) Source: https://nosmai.com/docs/moderation/concepts ## The four surfaces One license key unlocks four ways to moderate: | Surface | What it does | Method | | --- | --- | --- | | Image | Moderate a single photo | `analyzeImage` | | Video | Sample and aggregate a recorded clip | `analyzeVideo` | | Text | Moderate a chat message or comment | `moderateText` | | Live camera | Real-time per-frame moderation | `startStream` and frames | ## What it detects Objects. Each visual check returns the best confidence per class for `weapon`, `drug`, `cigarette` and `alcohol`. A class is flagged when its confidence meets that class's threshold. Adult / NSFW. A whole-image verdict: - `safe`: clean. - `warn`: suggestive or borderline. Advisory only, and it does not flag the content by itself. - `block`: explicit. This makes the content unsafe. Text. A message is classified as `safe`, `profanity`, `toxic`, `hate`, `harassment` or `threat`. Text runs in two layers: a fast keyword blocklist first, then an AI classifier for anything the blocklist misses. The result tells you which layer decided. ## Results A visual check (image or live frame) returns a result with: | Field | Type | Description | | --- | --- | --- | | `isUnsafe` | bool | True if any object is flagged, or NSFW is `block` | | `detections` | list | Flagged objects, each `{ category, confidence }` | | `nsfw` | enum | `safe`, `warn` or `block` | | `nsfwScores` | object | `safe`, `sexy`, `explicit` (0 to 1) | | `rawScores` | object | Best object score per class, for tuning | A text check returns `{ blocked, layer, category, score, matchedWord }`. A video check returns `{ isUnsafe, categories, flags, framesAnalyzed, nsfw }`, where `flags` lists the timestamps and categories that were flagged. ## Thresholds Every object class and both NSFW bars are adjustable at runtime. Lower is stricter. The defaults are tuned to balance catching unsafe content against false positives. ```text setThreshold(weapon, 0.70) // object class bar setNsfwThreshold(explicit, 0.45) // NSFW block bar setNsfwThreshold(sexy, 0.55) // NSFW warn bar ``` Start with the defaults. Tighten a threshold only if you see misses, and loosen one only if you see false positives for your content. ## Live performance On the live camera, the lighter NSFW check runs on every frame while the heavier object detector runs on a cadence you choose (favor responsiveness or battery). This keeps the camera preview smooth across a wide range of devices. See the platform guides for the controls. --- # iOS Platform Guide Source: https://nosmai.com/docs/moderation/ios ## Install Install via CocoaPods. Add the pod to your `Podfile` and run `pod install`. The SDK and its bundled models come with the pod, and everything runs on-device with no external dependencies. ```ruby pod 'NosmaiModerationSDK', '~> 1.0' ``` Requirements: iOS 15.1+, arm64. In `Info.plist` add: - `NSCameraUsageDescription`, required for the live-camera path. - `ITSAppUsesNonExemptEncryption` set to `NO`. The SDK only encrypts its own bundled models locally, which is export-compliance exempt. Without this, every TestFlight or App Store upload stalls on the encryption question. Consumers also need `ENABLE_USER_SCRIPT_SANDBOXING = NO` (Xcode 15+); a Podfile `post_install` can force it. ## Initialize `initialize` is blocking (network plus model load), so call it off the main thread. It returns `false` if the license is invalid or expired. ```swift import NosmaiDetection DispatchQueue.global(qos: .userInitiated).async { let ok = NosmaiSDK.initialize(licenseKey: "NOSMAI-XXXX") } ``` ## Moderate an image ```swift if let r = NosmaiSDK.analyzeImage(uiImage) { // NosmaiResult? if r.isUnsafe { // r.detections -> [NosmaiObjectDetection], r.nsfw -> .safe / .warn / .block } } ``` ## Moderate a video ```swift NosmaiSDK.analyze(videoURL: url, frameIntervalMs: 500, progress: { p in /* 0...1 */ }, completion: { result in /* result.isUnsafe, .categories, .flags */ }) ``` ## Moderate text Call `initialize` first, since it validates the license. `initializeText` needs that before it can load the text model. ```swift try? NosmaiSDK.initializeText() // after initialize(), off the main thread if let t = NosmaiSDK.moderateText("some message") { if t.blocked { /* t.layer, t.category, t.matchedWord */ } } ``` ## Live camera (AVCaptureSession) Feed pixel buffers from your `AVCaptureVideoDataOutput` to `pushFrame`. Results arrive via the `NosmaiListener`. ```swift NosmaiSDK.startStream(listener: self) // self conforms to NosmaiListener // in captureOutput(_:didOutput:from:) if let pb = CMSampleBufferGetImageBuffer(sampleBuffer) { NosmaiSDK.pushFrame(pb, rotationDegrees: 90) // 90 makes a back-camera portrait buffer upright } // on leave NosmaiSDK.stopStream() ``` Show the preview with an `AVCaptureVideoPreviewLayer` backed by the same `AVCaptureSession`. ## Thresholds ```swift NosmaiSDK.setThreshold(.weapon, value: 0.70) NosmaiSDK.setNsfwThreshold(.explicit, value: 0.45) // BLOCK NosmaiSDK.setNsfwThreshold(.sexy, value: 0.55) // WARN ``` ## Cleanup ```swift NosmaiSDK.shutdown() ``` `analyzeImage` and `moderateText` are synchronous and fast, but they still run inference, so call them off the main thread. `analyzeVideo` is async with a completion handler. --- # Android Platform Guide Source: https://nosmai.com/docs/moderation/android ## Install 1. Download the latest `nosmai-detection.aar` from the releases page. 2. Put it in your app module's `libs/` folder (for example `app/libs/nosmai-detection.aar`). 3. Reference it in Gradle (below). The SDK is self-contained: everything runs on-device and all models are bundled, so there are no extra dependencies to add. ```kotlin android { defaultConfig { minSdk = 24 ndk { abiFilters += "arm64-v8a" } // the SDK ships arm64-v8a } } dependencies { implementation(files("libs/nosmai-detection.aar")) // CameraX, only needed for the live-camera path val camerax = "1.4.2" implementation("androidx.camera:camera-core:$camerax") implementation("androidx.camera:camera-camera2:$camerax") implementation("androidx.camera:camera-lifecycle:$camerax") implementation("androidx.camera:camera-view:$camerax") } ``` The consuming app must use Kotlin 2.2.0 or later (the AAR ships Kotlin 2.2.0 metadata). Permissions (`AndroidManifest.xml`): `INTERNET` for the license check and `CAMERA` for the live path only. ## Initialize `init` is blocking (network plus model load), so call it off the main thread. It returns `false` if the license is invalid or expired. ```kotlin import com.nosmai.detection.NosmaiSDK Executors.newSingleThreadExecutor().execute { val ok = NosmaiSDK.init(context, "NOSMAI-XXXX") } ``` ## Moderate an image ```kotlin val bitmap = BitmapFactory.decodeFile(path) val r = NosmaiSDK.analyzeImage(bitmap) // NosmaiResult if (r.isUnsafe) { r.detections.forEach { Log.d("Mod", "${it.category} ${it.confidence}") } // r.nsfw -> SAFE / WARN / BLOCK } ``` ## Moderate a video ```kotlin NosmaiSDK.analyzeVideo( context, uri, frameIntervalMs = 500L, onProgress = { p -> /* 0..1 */ }, onComplete = { v -> /* v.isUnsafe, v.categories, v.flags */ }, ) ``` ## Moderate text Call `init` first, since it validates the license. `initText` needs that before it can load the text model. ```kotlin NosmaiSDK.initText(context) // after init(), off the main thread val t = NosmaiSDK.moderateText("some message") // NosmaiTextResult if (t.blocked) Log.d("Mod", "${t.category} via ${t.layer} (${t.matchedWord})") ``` ## Live camera (CameraX) Feed CameraX frames to `pushFrame`. Results arrive on the main thread via `NosmaiListener`. ```kotlin NosmaiSDK.startStream(object : NosmaiListener { override fun onResult(r: NosmaiResult) { /* every frame */ } override fun onUnsafe(r: NosmaiResult) { /* turned unsafe */ } override fun onSafe() { /* recovered */ } }) val analysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setOutputImageRotationEnabled(true) .build() .also { it.setAnalyzer(executor) { proxy -> NosmaiSDK.pushFrame(proxy) } } cameraProvider.bindToLifecycle(owner, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis) // on leave NosmaiSDK.stopStream() ``` ## Thresholds and performance ```kotlin NosmaiSDK.setThreshold(NosmaiCategory.WEAPON, 0.7f) NosmaiSDK.setNsfwThreshold(NosmaiNsfwClass.EXPLICIT, 0.45f) // BLOCK NosmaiSDK.setNsfwThreshold(NosmaiNsfwClass.SEXY, 0.55f) // WARN // Live only: how often the heavier object detector runs. NSFW always runs // every frame. HIGH is snappiest, LOW saves battery. NosmaiSDK.setPerformanceMode(NosmaiPerformanceMode.HIGH) ``` ## Cleanup ```kotlin NosmaiSDK.shutdown() ``` All `NosmaiListener` callbacks are delivered on the main thread, so you can update UI directly. --- # Flutter Platform Guide Source: https://nosmai.com/docs/moderation/flutter ## Install Add the plugin to `pubspec.yaml`, then run `flutter pub get`. ```yaml dependencies: nosmai_moderation_sdk: ^1.0.0 ``` Android. Bundle the native AAR in your host app. Download `nosmai-detection.aar` from the Android releases, put it in `android/app/libs/`, and reference it: ```kotlin android { defaultConfig { minSdk = 24 ndk { abiFilters += "arm64-v8a" } } } dependencies { implementation(files("libs/nosmai-detection.aar")) } ``` iOS needs no extra step. `pod install` pulls the native SDK with the plugin. Requirements: Flutter 3+, Android `minSdk 24` (arm64-v8a) with Kotlin 2.2+, iOS 15.1+. For live camera add the camera permission to `AndroidManifest.xml` and `NSCameraUsageDescription` to `Info.plist` (the example uses `permission_handler`). For iOS App Store uploads also set `ITSAppUsesNonExemptEncryption` to `NO` in `Info.plist`, since the SDK only encrypts its own bundled models, which is export-compliance exempt. Distribute Android as an App Bundle (AAB). The SDK ships `arm64-v8a` only, so Play hides it from 32-bit-only devices, and it does not run on x86_64 emulators. ## Initialize Call once at startup with your license key. It runs natively on a background thread. ```dart import 'package:nosmai_moderation_sdk/nosmai_moderation_sdk.dart'; final res = await NosmaiModeration.initialize('NOSMAI-XXXX'); if (!res.success) { debugPrint('Nosmai init failed: ${res.error}'); } ``` ## Moderate an image Pass a file path (for example from `image_picker`). Returns a `NosmaiResult`. ```dart final r = await NosmaiModeration.analyzeImage(file.path); if (r.isUnsafe) { // r.detections -> [{category, confidence}], r.nsfw -> safe / warn / block for (final d in r.detections) { print('${d?.category?.name} ${(d!.confidence! * 100).round()}%'); } } ``` ## Moderate a video Samples one frame every `frameIntervalMs` and aggregates. ```dart final v = await NosmaiModeration.analyzeVideo(file.path, 500); if (v.isUnsafe) { print('categories: ${v.categories}, frames: ${v.framesAnalyzed}'); } ``` ## Moderate text Call `initialize` first (it validates the license), then load the text model once (it is larger), then moderate messages. ```dart await NosmaiModeration.initializeText(); // after initialize() final t = await NosmaiModeration.moderateText('some message'); if (t.blocked) { // t.layer (blocklist or classifier), t.category, t.matchedWord print('blocked: ${t.category?.name}'); } ``` ## Live camera The camera, frame capture and detection all run natively. Only the per-frame `NosmaiResult` crosses to Dart. ```dart import 'package:permission_handler/permission_handler.dart'; // 1. show the native preview const NosmaiCameraPreview(); // 2. start and listen, after granting camera permission await Permission.camera.request(); final sub = NosmaiLive.results().listen( (r) => setState(() => _verdict = r.isUnsafe ? 'UNSAFE' : 'SAFE'), onError: (e) => debugPrint('camera failed to start: $e'), // permission or no camera ); await NosmaiLive.start(facing: NosmaiCameraFacing.back); // .front is also supported, and // falls back to the other if unavailable // 3. on leave, always stop. The camera does not stop itself on widget dispose. await NosmaiLive.stop(); sub.cancel(); ``` ## Thresholds Adjust any object class or NSFW bar at runtime (lower is stricter). ```dart NosmaiModeration.setThreshold(NosmaiCategory.weapon, 0.7); NosmaiModeration.setNsfwThreshold(NosmaiNsfwClass.explicit, 0.45); // BLOCK NosmaiModeration.setNsfwThreshold(NosmaiNsfwClass.sexy, 0.55); // WARN ``` ## Cleanup ```dart NosmaiModeration.shutdown(); ``` `analyzeImage`, `analyzeVideo`, `moderateText` and `initialize` are async. They run native inference off the platform thread, so the UI never blocks. --- # React Native Platform Guide Source: https://nosmai.com/docs/moderation/react-native ## Install ```sh npm install @nosmai/moderation-react-native ``` iOS. No extra step. The pod depends on `NosmaiModerationSDK`, so `cd ios && pod install` pulls the native SDK and its encrypted models. Set the Podfile platform to `15.1`, keep `ENABLE_USER_SCRIPT_SANDBOXING = NO` (the SDK is a static xcframework), and add `NSCameraUsageDescription` and `ITSAppUsesNonExemptEncryption = NO` to `Info.plist`. Android. The native SDK ships as a large AAR that the host app provides (the plugin references it at compile time only). Download `nosmai-detection.aar` from the Android releases into `android/app/libs/`, reference it, and use Kotlin 2.2.0+: ```gradle android { defaultConfig { minSdkVersion 24 ndk { abiFilters "arm64-v8a" } } } dependencies { implementation files("libs/nosmai-detection.aar") } ``` Requirements: React Native with the New Architecture (TurboModules), Android `minSdk 24` (arm64-v8a) with Kotlin 2.2+, iOS 15.1+. Distribute Android as an App Bundle (AAB); the SDK ships `arm64-v8a` only. ## Initialize Call once at startup, loading only the models you need. It runs natively on a background thread. ```tsx import { NosmaiModeration } from '@nosmai/moderation-react-native'; const res = await NosmaiModeration.initialize('NOSMAI-XXXX', ['objectDetection', 'nsfw']); if (!res.success) { console.warn('Nosmai init failed:', res.error); } ``` ## Moderate an image Pass a local file path (for example from `react-native-image-picker`). Returns a `NosmaiResult`. ```tsx const r = await NosmaiModeration.analyzeImage(filePath); if (r.isUnsafe) { // r.detections -> [{ category, confidence }], r.nsfw -> 'safe' | 'warn' | 'block' r.detections.forEach((d) => console.log(d.category, `${Math.round(d.confidence * 100)}%`)); } ``` ## Moderate a video Samples one frame every `frameIntervalMs` and aggregates. ```tsx const v = await NosmaiModeration.analyzeVideo(filePath, 500); if (v.isUnsafe) { console.log('categories:', v.categories, 'frames:', v.framesAnalyzed); } ``` ## Moderate text Load the text model once after `initialize`, then moderate messages. ```tsx await NosmaiModeration.initializeText(); // after initialize() const t = await NosmaiModeration.moderateText('some message'); if (t.blocked) { // t.layer -> 'blocklist' | 'classifier', t.category, t.matchedWord console.log('blocked:', t.category, t.layer, t.matchedWord); } ``` ## Live camera Render `` to open the camera and stream per-frame results. Mounting starts it, unmounting stops it. Only the per-frame `NosmaiResult` crosses into JS. Request the camera permission before mounting. ```tsx import { NosmaiCameraPreview } from '@nosmai/moderation-react-native'; setVerdict(r.isUnsafe ? 'UNSAFE' : 'SAFE')} /> ``` ## Thresholds Adjust any object class or NSFW bar at runtime (lower is stricter). ```tsx NosmaiModeration.setThreshold('weapon', 0.7); NosmaiModeration.setNsfwThreshold('explicit', 0.45); // BLOCK NosmaiModeration.setNsfwThreshold('sexy', 0.55); // WARN ``` ## Cleanup ```tsx NosmaiModeration.shutdown(); ``` `initialize`, `analyzeImage`, `analyzeVideo`, `moderateText` and `initializeText` are async and run native inference off the JS thread. Type enums are plain strings (`'weapon'`, `'safe' | 'warn' | 'block'`, `'explicit' | 'sexy'`), matching the iOS, Android and web SDKs. --- # Live Streaming (Agora) Source: https://nosmai.com/docs/moderation/live-streaming Nosmai moderates a live stream by tapping each captured video frame and running it through the SDK on-device, in real time. It works with any streaming SDK that exposes raw frames (Agora, LiveKit, WebRTC). Nothing about the moderation leaves the device; only the streaming SDK's own media uses the network. The pattern is the same everywhere: start the Nosmai stream with a listener (mobile) or an analysis loop (web); tap each captured frame from the streaming SDK; push it to Nosmai (`pushFrame` on mobile, `analyzeImage` on web); act on the SAFE / UNSAFE verdict. The tap is read-only, so the outgoing stream is never modified. Push a sampled subset of frames (about 3/sec), and do the conversion off the capture thread with drop-if-busy so the preview stays smooth. ## Example apps Full working Agora + Nosmai examples for every platform: https://github.com/nosmai/moderation-agora-examples (folders: `android/`, `ios/`, `web/`, `flutter_example/`). ## iOS (Swift + Agora) Become Agora's `AgoraVideoFrameDelegate`, request BGRA frames, and push each captured buffer. ```swift NosmaiSDK.startStream(listener: self) // self conforms to NosmaiListener func getVideoFormatPreference() -> AgoraVideoFormat { .cvPixelBGRA } func getRotationApplied() -> Bool { true } func onCapture(_ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType) -> Bool { if frameCount % 5 == 0, let buffer = videoFrame.pixelBuffer { NosmaiSDK.pushFrame(buffer, rotationDegrees: 0) } return true } func nosmaiOnResult(_ result: NosmaiResult) { /* result.isUnsafe, result.nsfw, ... */ } ``` ## Android (Kotlin + Agora) Register an `IVideoFrameObserver`, convert each I420 frame to a bitmap off the capture thread, and push it. ```kotlin NosmaiSDK.startStream(object : NosmaiListener { override fun onResult(result: NosmaiResult) { /* result.isUnsafe, ... */ } }) rtcEngine.registerVideoFrameObserver(object : IVideoFrameObserver { override fun onCaptureVideoFrame(sourceType: Int, frame: VideoFrame?): Boolean { if (frameCount % 5 == 0) worker.execute { // off-thread, drop-if-busy NosmaiSDK.pushFrame(i420ToBitmap(frame!!.buffer.toI420()), 0) } return true } override fun getVideoFrameProcessMode() = IVideoFrameObserver.PROCESS_MODE_READ_ONLY override fun getVideoFormatPreference() = IVideoFrameObserver.VIDEO_PIXEL_I420 }) ``` ## Web (JavaScript + Agora) Read the Agora local track into a `