npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rentlyorg/react-native-amazon-kvs

v0.3.0

Published

React Native bridge for Amazon Kinesis Video Streams (KVS), enabling native integration for real-time video streaming, signaling, and media communication on Android and iOS.

Downloads

439

Readme

@rentlyorg/react-native-amazon-kvs

React Native SDK for Amazon Kinesis Video Streams WebRTC, built on the New Architecture (TurboModules + Fabric).

Status: viewer and master (broadcaster) roles are both implemented, on iOS (Swift) and Android (Kotlin).

Both platforms follow the same control-plane → signaling → WebRTC flow as the official SDKs' sample apps (amazon-kinesis-video-streams-webrtc-sdk-ios, amazon-kinesis-video-streams-webrtc-sdk-android), wrapped as TurboModules/a Fabric component. A viewer connects and sends the SDP offer; a master listens on the signaling channel and replies with an SDP answer per connecting viewer, supporting any number of simultaneous viewers. iOS logic lives in ios/Kvs/*.swift (+ ios/Kvs*.{h,mm,swift}); Android logic lives in android/src/main/java/com/rentlyorg/reactnativeamazonkvs/*.kt.

Table of contents

Installation

npm install @rentlyorg/react-native-amazon-kvs
cd ios && pod install

Requires React Native's New Architecture to be enabled.

iOS Podfile requirement: this library's Swift code imports AWSKinesisVideo/AWSKinesisVideoSignaling directly, which requires those pods to publish a module map. Add to your app's Podfile (see example/ios/Podfile):

pod 'AWSKinesisVideo', :modular_headers => true
pod 'AWSKinesisVideoSignaling', :modular_headers => true

Without this, the build fails with no such module 'AWSKinesisVideo'.

iOS camera/mic usage descriptions: if your app broadcasts (or otherwise opens the camera/mic as a two-way viewer), add these to your app's Info.plist - without them, iOS crashes the process the instant the camera/mic is first accessed, permission dialog or not:

<key>NSCameraUsageDescription</key>
<string>Camera access is needed to broadcast video.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is needed to broadcast audio.</string>

Quick start

import { useEffect, useState } from 'react';
import {
  KvsPlayerView,
  KvsViewer,
  type KvsConnectionState,
} from '@rentlyorg/react-native-amazon-kvs';

function Viewer() {
  const [state, setState] = useState<KvsConnectionState>('idle');

  useEffect(() => {
    const sub = KvsViewer.addListener('onConnectionStateChanged', (e) =>
      setState(e.state)
    );
    return () => sub.remove();
  }, []);

  useEffect(() => {
    KvsViewer.configure({
      region: 'us-east-1',
      channelName: 'my-channel',
      // Fetch short-lived credentials from your own backend/STS/Cognito.
      // Do not ship long-lived IAM credentials in a released app.
      accessKeyId,
      secretAccessKey,
      sessionToken,
      // Pure one-way viewer - don't publish this device's camera/mic back
      // to the master (default is true/true, which does publish both).
      isAudioEnabled: false,
      isVideoEnabled: false,
    });
    KvsViewer.connect().catch(console.warn);
    return () => {
      KvsViewer.disconnect();
    };
  }, []);

  return (
    <>
      <Text>{state}</Text>
      <KvsPlayerView streamType="remote" style={{ flex: 1 }} />
    </>
  );
}

Broadcasting as a master looks the same shape, using KvsMaster instead and rendering the local camera preview instead of a remote stream - see KvsMaster and the full master example below.

KvsViewer

Singleton client - only one active viewer session per app is supported, matching the underlying native SDKs. Import as:

import { KvsViewer } from '@rentlyorg/react-native-amazon-kvs';

configure(config: KvsViewerConfig): void

Sets the channel/region/credentials for the session. Call this before every connect() - it's cheap and synchronous, so it's safe to call again to change channel/credentials between sessions.

KvsViewer.configure({
  region: 'us-east-1',
  channelName: 'my-channel',
  accessKeyId: 'ASIA...',
  secretAccessKey: '...',
  sessionToken: '...', // omit for long-lived IAM user keys (dev/testing only)
});

See KvsViewerConfig below for every field.

connect(): Promise<void>

Resolves the signaling channel ARN, fetches ICE server config, opens the signaling WebSocket, and negotiates the WebRTC connection as a viewer. Resolves once the signaling/WebRTC objects have been created and the connection attempt is underway - not once the connection is actually live. Watch onConnectionStateChanged for that.

Safe to call again at any time (e.g. to retry after a stuck/failed attempt) - it always tears down any previous session first and starts fresh.

try {
  await KvsViewer.connect();
} catch (error) {
  // Rejects if configure() was never called, if CAMERA/RECORD_AUDIO runtime
  // permission is missing while isAudioEnabled/isVideoEnabled is true, or if
  // the channel/ICE server control-plane lookups fail outright (e.g. bad
  // credentials, unknown channel name). Once the WebSocket/peer connection
  // actually opens, further failures surface via the `onError` event instead.
  console.warn(error);
}

disconnect(): Promise<void>

Tears down the peer connection and closes the signaling WebSocket. Safe to call at any time, including while a connect() attempt is still in progress (e.g. stuck waiting on a master that isn't up yet) or when already disconnected.

await KvsViewer.disconnect();

sendMessage(action: string, payload: string): Promise<void>

Sends an application-level message to the master over the signaling channel (not the WebRTC data channel). Rejects if not currently connected.

await KvsViewer.sendMessage('PING', JSON.stringify({ hello: 'world' }));

setLocalAudioEnabled(enabled: boolean): void

Enables/disables this device's outgoing microphone track. Only meaningful when isAudioEnabled: true was passed to configure() (i.e. two-way audio). No-op if there's no active local audio track.

KvsViewer.setLocalAudioEnabled(false); // mute

setLocalVideoEnabled(enabled: boolean): void

Enables/disables this device's outgoing camera track. Only meaningful when isVideoEnabled: true was passed to configure().

KvsViewer.setLocalVideoEnabled(false); // turn off local camera preview/publish

switchCamera(): void

Toggles the local capture device between front and back camera. No-op if there's no active local video track.

KvsViewer.switchCamera();

getConnectionState(): Promise<string>

Returns the current connection state as a plain string (one of the KvsConnectionState values). Prefer subscribing to onConnectionStateChanged for reactive UI; use this for a one-off imperative check.

const state = await KvsViewer.getConnectionState();

addListener(event, handler): EmitterSubscription

Subscribes to a native event. Returns a subscription object with .remove() - always clean this up (e.g. in a useEffect cleanup function).

const sub = KvsViewer.addListener('onError', (e) => {
  console.warn(e.code, e.message);
});
// later
sub.remove();

Viewer events reference

| Event | Payload | Fires when | | --- | --- | --- | | onConnectionStateChanged | { state: KvsConnectionState } | The session transitions between idle, connecting, connected, disconnected, failed. | | onError | { code: string; message: string } | Any control-plane, signaling, or WebRTC failure. code is a stable machine-readable string (e.g. channel_not_found, endpoint_lookup_failed, ice_server_lookup_failed, signing_failed, signaling_disconnected, not_configured, missing_permission) - safe to switch on. | | onRemoteStreamAdded | { hasAudio: boolean; hasVideo: boolean } | The master's remote audio/video track is received and attached. | | onRemoteStreamRemoved | {} | The remote stream goes away (disconnect, failure, or master leaves). Use this to stop showing a remote <KvsPlayerView> as live in your own UI, if you track that separately from onConnectionStateChanged. | | onDataMessage | { message: string } | An application message arrives from the master, either over the signaling channel (sendMessage on the master side) or the WebRTC data channel. |

KvsViewer.addListener('onRemoteStreamAdded', (e) => {
  console.log('remote video?', e.hasVideo, 'remote audio?', e.hasAudio);
});

KvsViewer.addListener('onDataMessage', (e) => {
  console.log('message from master:', e.message);
});

KvsMaster

Singleton client for the broadcaster role - only one active broadcast session per app is supported, but that single broadcast can have any number of simultaneous connected viewers (each gets its own WebRTC peer connection under the hood, sharing this device's camera/mic tracks). Import as:

import { KvsMaster } from '@rentlyorg/react-native-amazon-kvs';

configure(config: KvsMasterConfig): void

Sets the channel/region/credentials for the broadcast. Call this before every startBroadcast().

KvsMaster.configure({
  region: 'us-east-1',
  channelName: 'my-channel',
  accessKeyId: 'ASIA...',
  secretAccessKey: '...',
  sessionToken: '...',
});

See KvsMasterConfig below for every field. Unlike KvsViewerConfig, there's no clientId - a master doesn't identify itself with one; each connecting viewer's clientId is what's threaded through per-viewer events instead.

startBroadcast(): Promise<void>

Resolves the signaling channel ARN, fetches ICE server config, and opens the signaling WebSocket as a master - ready to accept viewer offers. Resolves once that setup completes, not once any viewer has connected. Watch onConnectionStateChanged for signaling readiness and onViewerConnected per viewer.

Safe to call again at any time - it always tears down any previous broadcast first (disconnecting every connected viewer) and starts fresh.

try {
  await KvsMaster.startBroadcast();
} catch (error) {
  // Rejects if configure() was never called, if CAMERA/RECORD_AUDIO runtime
  // permission is missing while isAudioEnabled/isVideoEnabled is true, or if
  // the channel/ICE server control-plane lookups fail outright. Once the
  // signaling WebSocket actually opens, further failures surface via the
  // `onError` event instead.
  console.warn(error);
}

stopBroadcast(): Promise<void>

Tears down every connected viewer's peer connection and closes the signaling WebSocket. Safe to call at any time, including while startBroadcast() is still in progress or when already stopped.

await KvsMaster.stopBroadcast();

sendMessage(action: string, payload: string, recipientClientId?: string): Promise<void>

Sends an application-level message over the signaling channel. Pass recipientClientId (from onViewerConnected) to target one viewer, or omit it to broadcast to every connected viewer. Rejects if not currently broadcasting.

// To everyone currently connected:
await KvsMaster.sendMessage('ANNOUNCEMENT', JSON.stringify({ text: 'hi' }));

// To one specific viewer:
await KvsMaster.sendMessage('PONG', 'ack', someClientId);

Master media/camera controls

Same shape as the viewer's - setLocalAudioEnabled(enabled: boolean): void, setLocalVideoEnabled(enabled: boolean): void, switchCamera(): void, and getConnectionState(): Promise<string>. These apply to the single shared local camera/mic feeding every connected viewer, not per-viewer.

KvsMaster.setLocalVideoEnabled(false); // pause outgoing camera for all viewers
KvsMaster.switchCamera();

addListener(event, handler): EmitterSubscription

Same as KvsViewer.addListener - see Master events reference for the event/payload shapes.

Master events reference

| Event | Payload | Fires when | | --- | --- | --- | | onConnectionStateChanged | { state: KvsConnectionState } | The broadcast session transitions between idle, connecting, connected (signaling open, ready to accept viewers), disconnected, failed. | | onError | { code: string; message: string } | Any control-plane, signaling, or WebRTC failure. Same code conventions as the viewer, plus answer_failed (failed to build an SDP answer for a connecting viewer). | | onViewerConnected | { clientId: string } | A viewer's peer connection reaches connected/completed ICE state. | | onViewerDisconnected | { clientId: string } | A previously-connected viewer's peer connection is disconnected/failed/closed, or stopBroadcast() tears down the session. | | onDataMessage | { clientId: string; message: string } | An application message arrives from a viewer, either over the signaling channel or a viewer's WebRTC data channel. clientId is empty for signaling-channel messages that don't carry a sender ID. |

KvsMaster.addListener('onViewerConnected', (e) => {
  console.log('viewer joined:', e.clientId);
});

KvsMaster.addListener('onDataMessage', (e) => {
  console.log('message from', e.clientId, ':', e.message);
});

<KvsPlayerView />

Native Fabric view that renders either a peer's incoming video or this device's own local camera preview. You can mount as many <KvsPlayerView>s as you like (e.g. a small local preview plus a fullscreen remote view) - they all attach to the single active KvsViewer/KvsMaster session, selected via sessionRole.

import { KvsPlayerView } from '@rentlyorg/react-native-amazon-kvs';

// Viewer, showing the master's remote stream:
<KvsPlayerView
  sessionRole="viewer" // default
  streamType="remote" // or "local"
  objectFit="contain" // or "cover" (default)
  mirror={false}
  style={{ flex: 1 }}
/>;

// Master, showing this device's own outgoing camera preview:
<KvsPlayerView sessionRole="master" streamType="local" style={{ flex: 1 }} />;

| Prop | Type | Default | Description | | --- | --- | --- | --- | | sessionRole | 'viewer' \| 'master' | 'viewer' | Which session this view attaches to. 'viewer' attaches to the single active KvsViewer session; 'master' attaches to the local camera preview of an active KvsMaster broadcast. | | streamType | 'remote' \| 'local' | 'remote' | 'remote' renders the peer's incoming track; 'local' renders this device's own outgoing camera preview. Only 'local' is meaningful when sessionRole="master" - a master may have many connected viewers, so there's no single "remote" track to render. | | objectFit | 'cover' \| 'contain' | 'cover' | 'cover' crops to fill the view (may zoom/crop); 'contain' letterboxes to show the full frame un-cropped. | | mirror | boolean | false | Horizontally flips the rendered video - typically used for a front-camera local preview. | | style | StyleProp<ViewStyle> | — | Standard RN style prop. |

A view with no live track yet (not connected, or the track hasn't arrived) renders as fully transparent/hidden - it never shows a stale frame from a previous session.

Types reference

import type {
  KvsViewerConfig,
  KvsMasterConfig,
  KvsIceServer,
  KvsConnectionState,
  KvsConnectionStateEvent,
  KvsErrorEvent,
  KvsRemoteStreamEvent,
  KvsDataMessageEvent,
  KvsEventMap,
  KvsEventName,
  KvsViewerClientEvent,
  KvsMasterDataMessageEvent,
  KvsMasterEventMap,
  KvsMasterEventName,
  KvsPlayerViewProps,
} from '@rentlyorg/react-native-amazon-kvs';

KvsViewerConfig

| Field | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | region | string | ✓ | — | AWS region, e.g. 'us-east-1'. | | channelName | string | ✓ | — | Existing KVS signaling channel name (this SDK only describes/connects to a channel - it doesn't create one). | | accessKeyId | string | ✓ | — | AWS access key. Use short-lived STS/Cognito credentials in production. | | secretAccessKey | string | ✓ | — | AWS secret key. | | sessionToken | string | | — | Required alongside accessKeyId/secretAccessKey when using temporary (STS/Cognito) credentials. | | clientId | string | | random UUID | Viewer client ID sent to the signaling channel. Auto-generated per session if omitted - only set this yourself if you need a stable/predictable ID. | | endpoint | string | | AWS default | Override the KVS control-plane endpoint. | | useDualStackEndpoint | boolean | | false | Use the dual-stack (api.aws) STUN host format instead of the classic amazonaws.com one. | | isAudioEnabled | boolean | | true | Publish this device's microphone to the master (two-way audio). Set false for a pure one-way viewer. | | isVideoEnabled | boolean | | true | Publish this device's camera to the master (two-way video). Set false for a pure one-way viewer. | | additionalIceServers | KvsIceServer[] | | [] | Extra ICE servers merged in alongside the ones KVS returns (e.g. your own TURN server). |

Note: isAudioEnabled/isVideoEnabled default to true, meaning this device's camera/mic are opened and published to the master by default (matching the two-way audio/video pattern in AWS's own KVS sample apps). If you only want to watch a stream, explicitly pass isAudioEnabled: false, isVideoEnabled: false - this also means CAMERA/RECORD_AUDIO permissions are never requested on Android.

KvsMasterConfig

Same fields as KvsViewerConfig except there's no clientId (a master doesn't have one of its own):

| Field | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | region | string | ✓ | — | AWS region, e.g. 'us-east-1'. | | channelName | string | ✓ | — | Existing KVS signaling channel name. | | accessKeyId | string | ✓ | — | AWS access key. Use short-lived STS/Cognito credentials in production. | | secretAccessKey | string | ✓ | — | AWS secret key. | | sessionToken | string | | — | Required alongside accessKeyId/secretAccessKey when using temporary (STS/Cognito) credentials. | | endpoint | string | | AWS default | Override the KVS control-plane endpoint. | | useDualStackEndpoint | boolean | | false | Use the dual-stack (api.aws) STUN host format instead of the classic amazonaws.com one. | | isAudioEnabled | boolean | | true | Open and publish this device's microphone to every connected viewer. A master with both this and isVideoEnabled false has nothing to broadcast. | | isVideoEnabled | boolean | | true | Open and publish this device's camera to every connected viewer. | | additionalIceServers | KvsIceServer[] | | [] | Extra ICE servers merged in alongside the ones KVS returns. |

KvsIceServer

type KvsIceServer = {
  urls: string[];
  username?: string;
  credential?: string;
};

KvsConnectionState

type KvsConnectionState =
  | 'idle'
  | 'connecting'
  | 'connected'
  | 'disconnected'
  | 'failed';

Shared by both KvsViewer and KvsMaster - for a master, 'connected' means the signaling channel is open and ready to accept viewers, not that any particular viewer is connected (see onViewerConnected/onViewerDisconnected for that).

Viewer event payload types

type KvsConnectionStateEvent = { state: KvsConnectionState };
type KvsErrorEvent = { code: string; message: string };
type KvsRemoteStreamEvent = { hasAudio: boolean; hasVideo: boolean };
type KvsDataMessageEvent = { message: string };

type KvsEventMap = {
  onConnectionStateChanged: KvsConnectionStateEvent;
  onError: KvsErrorEvent;
  onRemoteStreamAdded: KvsRemoteStreamEvent;
  onRemoteStreamRemoved: Record<string, never>;
  onDataMessage: KvsDataMessageEvent;
};

type KvsEventName = keyof KvsEventMap; // union of all event names above

Master event payload types

type KvsViewerClientEvent = { clientId: string };
type KvsMasterDataMessageEvent = { clientId: string; message: string };

type KvsMasterEventMap = {
  onConnectionStateChanged: KvsConnectionStateEvent;
  onError: KvsErrorEvent;
  onViewerConnected: KvsViewerClientEvent;
  onViewerDisconnected: KvsViewerClientEvent;
  onDataMessage: KvsMasterDataMessageEvent;
};

type KvsMasterEventName = keyof KvsMasterEventMap; // union of all event names above

KvsPlayerViewProps

type KvsPlayerViewProps = {
  style?: StyleProp<ViewStyle>;
  sessionRole?: 'viewer' | 'master'; // default 'viewer'
  streamType?: 'remote' | 'local'; // default 'remote'
  objectFit?: 'cover' | 'contain'; // default 'cover'
  mirror?: boolean; // default false
};

Full example (viewer screen)

A more complete example, matching the pattern used in example/src/App.tsx (status pill + connect/disconnect button + error surfacing):

import { useCallback, useEffect, useState } from 'react';
import { Button, Text, View } from 'react-native';
import {
  KvsPlayerView,
  KvsViewer,
  type KvsConnectionState,
} from '@rentlyorg/react-native-amazon-kvs';

export default function ViewerScreen() {
  const [state, setState] = useState<KvsConnectionState>('idle');
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const stateSub = KvsViewer.addListener('onConnectionStateChanged', (e) => {
      setState(e.state);
      if (e.state === 'connecting') setError(null);
    });
    const errorSub = KvsViewer.addListener('onError', (e) => {
      setError(e.message);
    });
    const removedSub = KvsViewer.addListener('onRemoteStreamRemoved', () => {
      console.log('remote stream ended');
    });
    return () => {
      stateSub.remove();
      errorSub.remove();
      removedSub.remove();
    };
  }, []);

  const connect = useCallback(() => {
    setError(null);
    KvsViewer.configure({
      region: 'us-east-1',
      channelName: 'my-channel',
      accessKeyId: 'ASIA...',
      secretAccessKey: '...',
      sessionToken: '...',
      isAudioEnabled: false,
      isVideoEnabled: false,
    });
    KvsViewer.connect().catch((e) => setError(String(e)));
  }, []);

  const disconnect = useCallback(() => {
    KvsViewer.disconnect();
  }, []);

  const isConnected = state === 'connected';
  const isBusy = state === 'connecting';

  return (
    <View style={{ flex: 1 }}>
      <KvsPlayerView streamType="remote" objectFit="contain" style={{ flex: 1 }} />
      <Text>{error ?? state}</Text>
      <Button
        title={isBusy ? 'Connecting…' : isConnected ? 'Disconnect' : 'Connect'}
        onPress={isConnected || isBusy ? disconnect : connect}
      />
    </View>
  );
}

Full example (master screen)

The broadcaster counterpart, matching MasterScreen in example/src/App.tsx - local camera preview, live viewer count, and a Go Live/Stop button. On Android, runtime permissions must be requested before startBroadcast() (see Android permissions):

import { useCallback, useEffect, useState } from 'react';
import { Button, PermissionsAndroid, Platform, Text, View } from 'react-native';
import {
  KvsPlayerView,
  KvsMaster,
  type KvsConnectionState,
} from '@rentlyorg/react-native-amazon-kvs';

async function requestMasterPermissions(): Promise<boolean> {
  if (Platform.OS !== 'android') return true;
  const results = await PermissionsAndroid.requestMultiple([
    PermissionsAndroid.PERMISSIONS.CAMERA,
    PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
  ]);
  return (
    results[PermissionsAndroid.PERMISSIONS.CAMERA] ===
      PermissionsAndroid.RESULTS.GRANTED &&
    results[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] ===
      PermissionsAndroid.RESULTS.GRANTED
  );
}

export default function MasterScreen() {
  const [state, setState] = useState<KvsConnectionState>('idle');
  const [error, setError] = useState<string | null>(null);
  const [viewerIds, setViewerIds] = useState<string[]>([]);

  useEffect(() => {
    const stateSub = KvsMaster.addListener('onConnectionStateChanged', (e) => {
      setState(e.state);
      if (e.state === 'connecting') setError(null);
      if (e.state === 'disconnected' || e.state === 'failed') setViewerIds([]);
    });
    const errorSub = KvsMaster.addListener('onError', (e) => setError(e.message));
    const connectedSub = KvsMaster.addListener('onViewerConnected', (e) => {
      setViewerIds((ids) => (ids.includes(e.clientId) ? ids : [...ids, e.clientId]));
    });
    const disconnectedSub = KvsMaster.addListener('onViewerDisconnected', (e) => {
      setViewerIds((ids) => ids.filter((id) => id !== e.clientId));
    });
    return () => {
      stateSub.remove();
      errorSub.remove();
      connectedSub.remove();
      disconnectedSub.remove();
      KvsMaster.stopBroadcast();
    };
  }, []);

  const start = useCallback(() => {
    setError(null);
    requestMasterPermissions()
      .then((granted) => {
        if (!granted) {
          setError('Camera and microphone permissions are required to broadcast.');
          return undefined;
        }
        KvsMaster.configure({
          region: 'us-east-1',
          channelName: 'my-channel',
          accessKeyId: 'ASIA...',
          secretAccessKey: '...',
          sessionToken: '...',
        });
        return KvsMaster.startBroadcast();
      })
      .catch((e) => setError(String(e)));
  }, []);

  const stop = useCallback(() => {
    KvsMaster.stopBroadcast();
  }, []);

  const isBroadcasting = state === 'connected';
  const isBusy = state === 'connecting';

  return (
    <View style={{ flex: 1 }}>
      <KvsPlayerView sessionRole="master" streamType="local" style={{ flex: 1 }} />
      <Text>
        {error ?? state} · {viewerIds.length} viewer{viewerIds.length === 1 ? '' : 's'}
      </Text>
      <Button
        title={isBusy ? 'Starting…' : isBroadcasting ? 'Stop broadcast' : 'Go Live'}
        onPress={isBroadcasting || isBusy ? stop : start}
      />
    </View>
  );
}

Android permissions

If you connect/broadcast with isAudioEnabled/isVideoEnabled left at their default (true), this device's camera/microphone are opened, and the library declares CAMERA/RECORD_AUDIO/MODIFY_AUDIO_SETTINGS in its manifest. Your app is still responsible for requesting those permissions at runtime before calling connect()/startBroadcast() - declaring them in the manifest alone is not enough past API 23:

import { PermissionsAndroid, Platform } from 'react-native';

if (Platform.OS === 'android') {
  await PermissionsAndroid.requestMultiple([
    PermissionsAndroid.PERMISSIONS.CAMERA,
    PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
  ]);
}

Both KvsViewer.connect() and KvsMaster.startBroadcast() check for the granted permission themselves before touching the camera/mic and reject with onError's missing_permission code if it's missing, rather than letting the underlying native AudioRecord/Camera2 initialization fail uncaught - but requesting permission proactively gives your users the system prompt at a sensible point in your UI instead of an immediate failure.

Skip this entirely by passing isAudioEnabled: false, isVideoEnabled: false for a pure one-way viewer - no camera/mic is ever opened, so no runtime permission prompt appears. (Not applicable to a master, which has nothing to broadcast with both disabled.)

Multiple viewers, one master

A KvsMaster broadcast supports any number of simultaneous connected viewers out of the box - each gets its own WebRTC peer connection (all fed the same local camera/mic tracks), tracked via the onViewerConnected/onViewerDisconnected events. Independently, multiple devices/app instances can each connect to the same KVS channel as separate KvsViewers - standard KVS WebRTC behavior. Each viewer device just needs its own (auto-generated, by default) clientId.

What is not supported is more than one active session of the same role from a single app instance - both KvsViewer and KvsMaster are singletons matching one active viewer session and one active broadcast per app, respectively. (A single app instance can run both at once, e.g. a viewer screen and a master screen mounted separately, since they're independent TurboModules.)

Contributing

License

MIT


Made with create-react-native-library