# 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 `