Integration
This guide covers how to integrate the Bambuser live player into a React Native application using the thin bridge approach. The native iOS (Swift) and Android (Kotlin/Compose) SDKs do the actual video work; the React Native layer is a thin bridge that passes configuration and events.
Prerequisites
- React Native 0.71+
- iOS 15.6+ / Android API 26+
- Bambuser Commerce SDK installed for iOS and Android (see iOS Installation and Android Installation)
iOS Native Bridge
1. Add the SDK dependency
In your ios/Podfile:
pod 'BambuserCommerceSDK', :git => 'https://github.com/bambuser/bambuser-commerce-sdk-ios.git', :tag => '2.0.1'
Then run pod install.
2. Create the native view
Create ios/BambuserVideoView.swift. This wraps the iOS SDK player and exposes it to React Native:
import UIKit
import BambuserCommerceSDK
@objc(BambuserVideoView)
class BambuserVideoView: UIView, BambuserVideoPlayerDelegate {
// MARK: - Props from React Native
@objc var server: String = "US"
@objc var mode: String = "live"
@objc var id: String = "" { didSet { rebuildIfMounted() } }
@objc var events: [String] = ["*"] { didSet { rebuildIfMounted() } }
@objc var configuration: NSDictionary = [:] { didSet { rebuildIfMounted() } }
// MARK: - Event callbacks
@objc var onEvent: RCTDirectEventBlock?
@objc var onStatus: RCTDirectEventBlock?
@objc var onError: RCTDirectEventBlock?
@objc var onProgress: RCTDirectEventBlock?
private var playerView: (any BambuserPlayerView)?
private var bambuserPlayer: BambuserVideoPlayer?
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil { rebuild() }
}
private func rebuildIfMounted() {
if window != nil { rebuild() }
}
private func rebuild() {
playerView?.cleanup()
playerView?.removeFromSuperview()
let server: OrganizationServer = (self.server == "EU") ? .EU : .US
let factory = BambuserVideoPlayer(server: server)
let config = BambuserVideoConfiguration(
type: .live(id: self.id),
events: self.events,
configuration: configuration as? [String: Sendable] ?? [:]
)
let pView = factory.createPlayerView(videoConfiguration: config)
pView.delegate = self
pView.translatesAutoresizingMaskIntoConstraints = false
addSubview(pView)
NSLayoutConstraint.activate([
pView.topAnchor.constraint(equalTo: topAnchor),
pView.leadingAnchor.constraint(equalTo: leadingAnchor),
pView.trailingAnchor.constraint(equalTo: trailingAnchor),
pView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
playerView = pView
}
// MARK: - BambuserVideoPlayerDelegate
func onNewEventReceived(_ id: String, event: BambuserEventPayload) {
onEvent?([
"playerId": id,
"type": event.type,
"data": event.data,
"callbackKey": event.data["callbackKey"] as? String ?? NSNull(),
])
}
func onErrorOccurred(_ id: String, error: Error) {
onError?(["playerId": id, "message": error.localizedDescription])
}
func onVideoStatusChanged(_ id: String, state: BambuserVideoState) {
onStatus?(["playerId": id, "state": "\(state)"])
}
func onVideoProgress(_ id: String, duration: Double, currentTime: Double) {
onProgress?(["playerId": id, "duration": duration, "currentTime": currentTime])
}
// MARK: - Imperative commands
func notifyPlayer(callbackKey: String, info: Any) {
playerView?.notify(callbackKey: callbackKey, info: info)
}
func invokePlayer(function: String, arguments: String) async throws -> Any? {
return try await playerView?.invoke(function: function, arguments: arguments)
}
}
3. Create the view manager
Create ios/BambuserVideoViewManager.swift:
@objc(BambuserVideoViewManager)
class BambuserVideoViewManager: RCTViewManager {
override func view() -> UIView! {
BambuserVideoView()
}
override static func requiresMainQueueSetup() -> Bool { true }
@objc func notify(_ reactTag: NSNumber, callbackKey: String, info: Any) {
bridge.uiManager.addUIBlock { manager, viewRegistry in
guard let view = viewRegistry?[reactTag] as? BambuserVideoView else { return }
view.notifyPlayer(callbackKey: callbackKey, info: info)
}
}
@objc func invoke(_ reactTag: NSNumber, function: String, arguments: String,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock) {
bridge.uiManager.addUIBlock { manager, viewRegistry in
guard let view = viewRegistry?[reactTag] as? BambuserVideoView else {
rejecter("NOT_FOUND", "Player view not found", nil)
return
}
Task {
do {
let result = try await view.invokePlayer(function: function, arguments: arguments)
resolver(result)
} catch {
rejecter("INVOKE_FAILED", error.localizedDescription, error)
}
}
}
}
}
Create ios/BambuserVideoViewManager.m to expose the methods to the bridge:
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>
RCT_EXTERN_MODULE(BambuserVideoViewManager, RCTViewManager)
RCT_EXTERN_METHOD(notify:(nonnull NSNumber *)reactTag callbackKey:(NSString *)callbackKey info:(id)info)
RCT_EXTERN_METHOD(invoke:(nonnull NSNumber *)reactTag function:(NSString *)function arguments:(NSString *)arguments
resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter)
Android Native Bridge
1. Create the native view
Create android/app/src/main/java/.../BambuserVideoReactView.kt:
class BambuserVideoReactView(context: Context) : AbstractComposeView(context) {
var videoId = mutableStateOf("")
var server = mutableStateOf("US")
var eventsConfig = mutableStateOf(listOf("*"))
var configuration = mutableStateOf(emptyMap<String, Any>())
private var viewActionsRef: ViewActions? = null
var onEvent: ((WritableMap) -> Unit)? = null
var onError: ((WritableMap) -> Unit)? = null
@Composable
override fun Content() {
val sdk = (context.applicationContext as HostApplication).globalBambuserSDK
sdk.GetLiveView(
modifier = Modifier.fillMaxSize(),
videoConfiguration = BambuserVideoConfiguration(
events = eventsConfig.value,
configuration = configuration.value,
videoType = BambuserVideoAsset.Live(videoId.value)
),
videoPlayerDelegate = object : BambuserVideoPlayerDelegate {
override fun onNewEventReceived(playerId: String, event: BambuserEventPayload, viewAction: ViewActions) {
viewActionsRef = viewAction
val body = Arguments.createMap()
body.putString("playerId", playerId)
body.putString("type", event.event)
event.callbackKey?.let { body.putString("callbackKey", it) }
onEvent?.invoke(body)
}
override fun onErrorOccurred(playerId: String, error: Exception) {
val body = Arguments.createMap()
body.putString("playerId", playerId)
body.putString("message", error.message ?: "Unknown error")
onError?.invoke(body)
}
}
)
}
fun notify(callbackKey: String, info: Any) {
viewActionsRef?.notifyView(callbackKey, info)
}
fun invoke(function: String, arguments: String, onDone: (Any?) -> Unit, onErr: (Exception) -> Unit) {
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
try {
val result = viewActionsRef?.invoke(function, arguments)
onDone(result)
} catch (e: Exception) {
onErr(e)
}
}
}
}
2. Create the view manager
Create android/app/src/main/java/.../BambuserVideoViewManager.kt:
class BambuserVideoViewManager : SimpleViewManager<BambuserVideoReactView>() {
override fun getName() = "BambuserVideoView"
override fun createViewInstance(context: ThemedReactContext) = BambuserVideoReactView(context)
override fun getExportedCustomDirectEventTypeConstants() = MapBuilder.of(
"onEvent", MapBuilder.of("registrationName", "onEvent"),
"onError", MapBuilder.of("registrationName", "onError"),
)
@ReactProp(name = "id")
fun setId(view: BambuserVideoReactView, id: String) { view.videoId.value = id }
@ReactProp(name = "configuration")
fun setConfiguration(view: BambuserVideoReactView, config: ReadableMap) {
view.configuration.value = config.toHashMap()
}
}
JavaScript / TypeScript Component
Create src/native/BambuserVideoView.tsx:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import {
requireNativeComponent,
UIManager,
findNodeHandle,
NativeModules,
ViewStyle,
} from 'react-native';
const NativeBambuserVideoView = requireNativeComponent('BambuserVideoView');
export interface BambuserVideoViewRef {
notify: (callbackKey: string, info: boolean | string) => void;
invoke: (fn: string, args: string) => Promise<any>;
}
interface Props {
style?: ViewStyle;
server?: 'US' | 'EU';
mode: 'live';
id: string;
events?: string[];
configuration?: Record<string, any>;
onEvent?: (event: any) => void;
onStatus?: (event: { playerId: string; state: string }) => void;
onError?: (event: { playerId: string; message: string }) => void;
onProgress?: (event: { playerId: string; duration: number; currentTime: number }) => void;
}
const BambuserVideoView = forwardRef<BambuserVideoViewRef, Props>((props, ref) => {
const nativeRef = useRef(null);
useImperativeHandle(ref, () => ({
notify(callbackKey, info) {
const tag = findNodeHandle(nativeRef.current);
NativeModules.BambuserVideoViewManager.notify(tag, callbackKey, info);
},
invoke(fn, args) {
const tag = findNodeHandle(nativeRef.current);
return NativeModules.BambuserVideoViewManager.invoke(tag, fn, args);
},
}));
return (
<NativeBambuserVideoView
ref={nativeRef}
{...props}
onEvent={(e: any) => props.onEvent?.(e.nativeEvent)}
onStatus={(e: any) => props.onStatus?.(e.nativeEvent)}
onError={(e: any) => props.onError?.(e.nativeEvent)}
onProgress={(e: any) => props.onProgress?.(e.nativeEvent)}
/>
);
});
export default BambuserVideoView;
Usage: Live Player with Events
import React, { useRef } from 'react';
import { StyleSheet, Platform } from 'react-native';
import BambuserVideoView, { BambuserVideoViewRef } from './native/BambuserVideoView';
export default function LivePlayerScreen() {
const playerRef = useRef<BambuserVideoViewRef>(null);
const handleEvent = (payload: any) => {
const type = payload.type;
// Platform-specific payload paths:
// iOS: callbackKey in payload.data.callbackKey | event data in payload.data.event.*
// Android: callbackKey in payload.callbackKey | event data in payload.data.*
const callbackKey = Platform.OS === 'ios'
? payload.data?.callbackKey
: payload.callbackKey;
if (type === 'provide-product-data') {
const products = Platform.OS === 'ios'
? payload.data?.event?.products
: payload.data?.products;
products?.forEach((product: any) => {
const id = product.id;
const sku = product.ref;
const hydrationJson = buildProductJson(sku);
playerRef.current?.invoke('updateProductWithData', `'${id}', ${hydrationJson}`);
});
}
if (type === 'should-add-item-to-cart' && callbackKey) {
const sku = Platform.OS === 'ios'
? payload.data?.event?.sku
: payload.data?.sku;
addToCart(sku).then(success => {
playerRef.current?.notify(callbackKey, success || "{ success: false, reason: 'out-of-stock' }");
});
}
};
return (
<BambuserVideoView
ref={playerRef}
style={StyleSheet.absoluteFill}
mode="live"
id="your-show-id"
configuration={{
autoplay: true,
currency: 'USD',
locale: 'en-US',
buttons: { dismiss: 'none' },
ui: { hideEmojiOverlay: true },
}}
onEvent={handleEvent}
onError={e => console.error('Player error:', e.message)}
/>
);
}
Notes
invokeis asynchronous and returns a Promise.notifyis synchronous — call it from the event handler (no await needed).- Call
cleanup()on the native module when the screen unmounts to release player resources. - For PiP support on iOS, call
startPiP()/stopPiP()through the view manager.
Demo application
We have a demo application using our bridge setup on GitHub. This repo is private, check with our support team to get access.