Bambuser Thin Layer
The thin layer is a native bridge that exposes the Bambuser iOS and Android SDKs to React Native. There is no official Bambuser React Native package — the thin layer is a hand-rolled native bridge that you implement once and own.
Architecture
React Native JS
↕ props / events (high-level only)
Native Bridge (iOS Swift / Android Kotlin)
↕ SDK API calls
BambuserCommerceSDK (native, compiled)
↕ WebView messaging
Player WebView (UI layer)
The bridge is "thin" because only high-level commands (start player, notify, invoke) cross the JS/native boundary. Video frames, network I/O, and all real-time processing stay fully native.
JavaScript API
BambuserVideoView Component
import BambuserVideoView, { BambuserVideoViewRef } from './native/BambuserVideoView';
Props:
| Prop | Type | Required | Description |
|---|---|---|---|
mode | 'live' | Yes | Video mode — always 'live' for live shows |
id | string | Yes | Show ID |
server | 'US' | 'EU' | No | Organization server region (default 'US') |
events | string[] | No | Events to listen for (default ['*'] = all) |
configuration | Record<string, any> | No | Player configuration (buttons, ui, autoplay, currency, locale...) |
onEvent | (event: PlayerEvent) => void | No | Fired when the player emits an event |
onStatus | (event: StatusEvent) => void | No | Fired when player state changes |
onError | (event: ErrorEvent) => void | No | Fired when an error occurs |
onProgress | (event: ProgressEvent) => void | No | Fired periodically with playback progress |
Ref methods (imperative API):
| Method | Description |
|---|---|
notify(callbackKey: string, info: any): void | Synchronously responds to a player event that carries a callbackKey |
invoke(fn: string, args: string): Promise<any> | Asynchronously calls a player function (e.g. updateProductWithData) |
Event Payload Structure
Events received via onEvent carry a platform-specific payload structure:
| Field | iOS path | Android path |
|---|---|---|
| Event type | payload.type | payload.type |
| Event data | payload.data.event.* | payload.data.* |
callbackKey | payload.data.callbackKey | payload.callbackKey |
Always check Platform.OS when extracting event data:
const callbackKey = Platform.OS === 'ios'
? payload.data?.callbackKey
: payload.callbackKey;
const sku = Platform.OS === 'ios'
? payload.data?.event?.sku
: payload.data?.sku;
iOS Bridge
Files
| File | Purpose |
|---|---|
BambuserVideoView.swift | UIView wrapping BambuserPlayerView; implements BambuserVideoPlayerDelegate |
BambuserVideoViewManager.swift | RCTViewManager exposing the view to React Native |
BambuserVideoViewManager.m | Obj-C bridge file with RCT_EXTERN_METHOD declarations |
Key iOS Implementation Details
Rebuilding the player — The view calls rebuild() when any of id, events, or configuration props change (and the view is mounted in a window):
@objc var id: String = "" { didSet { rebuildIfMounted() } }
Delegate events — All BambuserVideoPlayerDelegate callbacks translate to React Native events:
func onNewEventReceived(_ id: String, event: BambuserEventPayload) {
onEvent?([
"playerId": id,
"type": event.type,
"data": event.data, // includes "callbackKey" on iOS
"callbackKey": event.data["callbackKey"] as? String ?? NSNull(),
])
}
notify command — Calls playerView.notify(callbackKey:info:) on the main queue:
@objc func notify(_ reactTag: NSNumber, callbackKey: String, info: Any) {
bridge.uiManager.addUIBlock { _, viewRegistry in
guard let view = viewRegistry?[reactTag] as? BambuserVideoView else { return }
view.playerView?.notify(callbackKey: callbackKey, info: info)
}
}
invoke command — Async call via BambuserPlayerView.invoke(function:arguments:), resolved/rejected as a JS Promise:
@objc func invoke(_ reactTag: NSNumber, function: String, arguments: String,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock) {
bridge.uiManager.addUIBlock { _, viewRegistry in
guard let view = viewRegistry?[reactTag] as? BambuserVideoView else {
rejecter("NOT_FOUND", "View not found", nil); return
}
Task {
do {
let result = try await view.playerView?.invoke(function: function, arguments: arguments)
resolver(result)
} catch {
rejecter("INVOKE_FAILED", error.localizedDescription, error)
}
}
}
}
PiP support — startPiP() and stopPiP() delegate to playerView.pipController?.start() / .stop().
Android Bridge
Files
| File | Purpose |
|---|---|
BambuserVideoReactView.kt | AbstractComposeView wrapping GetLiveView; holds ViewActions ref |
BambuserVideoViewManager.kt | SimpleViewManager exposing the Compose view as BambuserVideoView |
BambuserVideoViewModule.kt | ReactContextBaseJavaModule (NativeModules.BambuserVideoViewManager) exposing invoke, notify, cleanup as @ReactMethod |
BambuserPackage.kt | Registers all modules with the React Native package |
Key Android Implementation Details
callbackKey is top-level — Android's BambuserEventPayload has callbackKey as a first-class field, not inside event.data:
override fun onNewEventReceived(playerId: String, event: BambuserEventPayload, viewAction: ViewActions) {
viewActionsRef = viewAction
val body = Arguments.createMap()
body.putString("type", event.event) // event type string
event.callbackKey?.let {
body.putString("callbackKey", it) // top-level on Android
}
onEvent?.invoke(body)
}
notifyView not notify — The Android API uses viewActions.notifyView(callbackKey, info):
fun notify(callbackKey: String, info: Any) {
viewActionsRef?.notifyView(callbackKey, info)
}
invoke runs in a coroutine — ViewActions.invoke() is a suspend function:
fun invoke(function: String, arguments: String, onDone: (Any?) -> Unit, onErr: (Exception) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
try {
val result = viewActionsRef?.invoke(function, arguments)
onDone(result)
} catch (e: Exception) {
onErr(e)
}
}
}
Performance
The thin bridge has no performance cost over a pure native implementation because:
- No high-frequency data crosses the bridge — video frames, audio, and network I/O stay fully native.
- Thread isolation — decoding and buffering run on native threads; the React Native JS thread is never blocked.
- Commands are infrequent —
invokeandnotifyare called a handful of times per user interaction, not per frame.
The bridge acts as a remote control: it sends commands and receives lifecycle events, but all real-time work stays native.