All five LevelChat SDKs — Web, iOS, Android, React Native, and Flutter — expose the same capabilities. The Web SDK is the canonical reference; every other platform mirrors its method names, parameters, and event shape unless a platform constraint forces a documented divergence.
This page is the table you want open when you port integration code from one stack to another.
Method surface
| Capability | Web | iOS | Android | React Native | Flutter | Notes / divergence |
|---|---|---|---|---|---|---|
new LevelChat({ apiKey?, region? }) | yes | yes | yes | yes | yes | iOS/Android use a factory: try await LevelChat(config:) / LevelChat.create(context, config). |
client.issueToken({ userId, roomId, ... }) | yes | yes | yes | yes | yes | Dev only — mint tokens server-side in production. See Quickstart. |
client.joinRoom(...) | yes | yes | yes | yes | yes | Web/iOS/Android take a single options object; RN + Flutter take (roomId, options). Returns Room. |
client.joinLive({ token, role }) | yes | yes | yes | yes | yes | Returns a LiveStream { room, role } on every stack. |
room.publishCamera({ resolution, ... }) | yes | yes | yes | yes | yes | Simulcast on by default. |
room.publishMic({ ... }) | yes | yes | yes | publishMicrophone | publishMicrophone | Web/iOS/Android: publishMic. RN + Flutter spell it publishMicrophone — see Known divergences. |
room.publishScreen({ audio }) | yes | yes | yes | yes | yes | iOS needs a Broadcast Upload Extension; Android needs a mediaProjection foreground service. |
room.stopPublishing(trackId) | yes | yes | yes | unpublish(source) | unpublish(source) | Web/iOS/Android: stopPublishing(trackId). RN + Flutter: unpublish(source) — see Known divergences. |
| Leave the room | leave() | leave() | leave() | close([reason]) | close([reason]) | RN + Flutter spell it close([reason]) — see Known divergences. |
| Device enumeration | DeviceManager.list*() | DeviceManager.list*() | DeviceManager.list*() | enumerateDevices() | mediaDevices | Web/iOS/Android: a DeviceManager with listCameras / listMicrophones / listSpeakers. RN exposes one combined enumerateDevices(); Flutter delegates to mediaDevices. facing: front/back is normalised. |
| Audio session (voice-call mode) | yes | yes | yes | yes | yes | iOS: AVAudioSession; Android: AudioManager + audio focus. |
| Lifecycle observer (auto-pause camera in background) | yes | yes | yes | yes | yes | Web uses the Page Visibility API. |
| Network quality scorer (5-band) | yes | yes | yes | yes | yes | Same thresholds on every platform. |
| Room reconnect (capped exponential backoff) | yes | yes | yes | yes | yes | |
| AES-GCM SFrame primitives (out-of-band E2EE) | yes | yes | yes | yes | yes | Cross-platform 9-byte wire format. |
| Frame-transformer integration (in-band E2EE) | yes | no | yes | no | no | See Known divergences — iOS, RN, and Flutter fall back to SRTP + the SFrame primitives. |
Event surface
All SDKs deliver the same logical events with an identical semantic payload. Both the carrier and the wire name differ by platform idiom:
- Carrier —
room.on('…', cb)on Web and React Native,AsyncStream<RoomEvent>on iOS,Flow<RoomEvent>on Android,Stream<RoomEvent>on Flutter. - Wire name — Web emits kebab-case strings (
'participant-joined'), React Native emits camelCase strings ('participantJoined'), and iOS/Android model events as enum cases (.participantJoined/RoomEvent.ParticipantJoined). Apps never hand-type these — they subscribe through typed helpers — so the spelling divergence is cosmetic, but it is a divergence and is recorded here rather than papered over.
| Logical event | Web | iOS | Android | React Native | Flutter |
|---|---|---|---|---|---|
connected / disconnected | yes | yes | yes | yes | yes |
reconnecting / reconnected | yes | yes | yes | yes | yes |
closed | yes | yes | yes | yes | yes |
participantJoined | yes | yes | yes | yes | yes |
participantLeft | yes | yes | yes | yes | yes |
trackPublished | yes | yes | yes | yes | yes |
trackUnpublished | yes | yes | yes | yes | yes |
trackSubscribed | yes | yes | yes | yes | yes |
trackUnsubscribed | yes | yes | yes | yes | yes |
qualityChanged | yes | yes | yes | yes | yes |
Error-code namespace
Every platform uses the same stable string codes. Apps switch on error.code, never on
error.message.
| Namespace | Examples |
|---|---|
config/* | config/invalid, config/missing-api-key |
token/* | token/invalid, token/expired, token/unauthorized |
signaling/* | signaling/connect-failed, signaling/closed, signaling/timeout, signaling/protocol |
transport/* | transport/ice-failed, transport/dtls-failed, transport/unsupported |
media/* | media/permission-denied, media/device-not-found, media/unsupported, media/constraint-not-satisfied |
room/* | room/full, room/ended, room/kicked, room/cap-denied |
codec/* | codec/unsupported |
encryption/* | encryption/key-missing, encryption/unsupported |
recording/* | recording/not-permitted, recording/backend-failed |
network/* | network/unreachable, network/rate-limited |
internal/* | internal |
The Web SDK page lists the full code enum with the matching
LevelChatError subclasses.
Known divergences
React Native / Flutter naming dialect
Web, iOS, and Android share one method-naming dialect; React Native and Flutter share another. The capabilities are behaviourally identical — only the public spelling differs. An integrator porting code between stacks needs this table:
| Capability | Web / iOS / Android | React Native / Flutter |
|---|---|---|
| Leave the room | room.leave() | room.close([reason]) |
| Stop a publish | room.stopPublishing(id) | room.unpublish(source) |
| Publish mic | room.publishMic(...) | room.publishMicrophone(...) |
| Device enumeration | DeviceManager.list*() | enumerateDevices() (RN); mediaDevices (Flutter) |
This split predates the parity matrix — RN and Flutter shipped first with the DOM-flavoured names and we kept them for backwards compatibility rather than break early adopters.
Track handles — React Native / Flutter only
React Native and Flutter return mutable LocalTrack / RemoteTrack handles with instance
methods (pause(), resume(), close(), RemoteTrack.setPreferredLayer(...)). Web, iOS,
and Android instead model a published track as an immutable TrackView value and do
track lifecycle through room-level methods (room.stopPublishing(id)) and the SFU's
signaling-driven layer selection — there is no track object to call methods on. This is an
RN/Flutter-only convenience layer, not a parity gap.
End-to-end encryption — two layers
SFrame primitives (out-of-band) ship on all five SDKs: AES-GCM key wrapping plus an
SFrame-style 9-byte header (magic | keyId(2) | counter(6)), with the same wire format
across every platform. Apps that want to pre-encrypt frames before publishing call the
public EncryptionContext + SFrame.encrypt/decrypt directly.
Frame-transformer integration (in-band) is where platforms diverge:
- Web — supported on Chromium via
RTCRtpSender.transform. - Android — shipped via libwebrtc's
FrameCryptor.Room.setEncryptionKey(...)rotates a shared key provider that every per-sender and per-receiver cryptor references. AES-GCM, SFrame-compatible wire format with the Web SDK. - iOS — upstream-blocked. The public
WebRTC.frameworkdistribution only exposesRTCCryptoOptionsfor SRTP cipher selection; the FrameCryptor API isn't bridged. - React Native —
react-native-webrtcdoes not expose insertable streams. The SDK accepts thee2ee:flag for API parity, logs a warning, and falls back to SRTP plus the AES-GCM SFrame primitives for out-of-band encryption. - Flutter — same as React Native.
Native render path
- Web —
<video srcObject={track.mediaStreamTrack}>. - iOS —
RTCMTLVideoViewfrom WebRTC.framework, attached viatrack.nativeTrack. - Android —
SurfaceViewRendererfrom libwebrtc. - React Native —
<RTCView streamURL={track.nativeTrack.id}>. - Flutter —
<RTCVideoView>fed via anRTCVideoRendererinitialised with the track's parent stream.
How parity is enforced
- The Web SDK is the authoritative spec.
- Every other SDK mirrors its method names, parameters, and event shape.
- The shared error-code namespace lives in
@levelchat/shared-types, so a typo on any platform fails to compile against the canonical enum. - Each SDK ships its own unit-test suite, and a shared integration suite runs one fixture against all five SDKs.
If you hit a behavioural difference that isn't documented on this page, it's a bug — email support@levelchat.io.