Skip to main content

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:

PropTypeRequiredDescription
mode'live'YesVideo mode — always 'live' for live shows
idstringYesShow ID
server'US' | 'EU'NoOrganization server region (default 'US')
eventsstring[]NoEvents to listen for (default ['*'] = all)
configurationRecord<string, any>NoPlayer configuration (buttons, ui, autoplay, currency, locale...)
onEvent(event: PlayerEvent) => voidNoFired when the player emits an event
onStatus(event: StatusEvent) => voidNoFired when player state changes
onError(event: ErrorEvent) => voidNoFired when an error occurs
onProgress(event: ProgressEvent) => voidNoFired periodically with playback progress

Ref methods (imperative API):

MethodDescription
notify(callbackKey: string, info: any): voidSynchronously 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:

FieldiOS pathAndroid path
Event typepayload.typepayload.type
Event datapayload.data.event.*payload.data.*
callbackKeypayload.data.callbackKeypayload.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

FilePurpose
BambuserVideoView.swiftUIView wrapping BambuserPlayerView; implements BambuserVideoPlayerDelegate
BambuserVideoViewManager.swiftRCTViewManager exposing the view to React Native
BambuserVideoViewManager.mObj-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 supportstartPiP() and stopPiP() delegate to playerView.pipController?.start() / .stop().


Android Bridge

Files

FilePurpose
BambuserVideoReactView.ktAbstractComposeView wrapping GetLiveView; holds ViewActions ref
BambuserVideoViewManager.ktSimpleViewManager exposing the Compose view as BambuserVideoView
BambuserVideoViewModule.ktReactContextBaseJavaModule (NativeModules.BambuserVideoViewManager) exposing invoke, notify, cleanup as @ReactMethod
BambuserPackage.ktRegisters 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 coroutineViewActions.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 infrequentinvoke and notify are 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.