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

react-native-nitro-osc

v0.2.0

Published

react-native-nitro-osc

Readme

react-native-nitro-osc

OSC (Open Sound Control) for React Native, powered by Nitro Modules.

Send and receive OSC messages over UDP — turn a phone into a control surface for lighting desks, VJ software, DAWs, synths, or anything else that speaks OSC.

The codec and the socket are independent. Encode a message and send it however you like, or hand the socket bytes that never came from this library at all.


Platform support

| Platform | Status | |---|---| | iOS (physical device) | Supported — sending verified on hardware | | iOS Simulator | Works, but Local Network permission behaves differently than on device — verify on hardware | | Android | Implemented and tested against loopback traffic; not yet verified on a physical device |


Prerequisites


Installation

npm install react-native-nitro-osc react-native-nitro-modules
cd ios && pod install

Android needs no setup beyond the INTERNET permission, which the React Native app template already declares. iOS needs one Info.plist key — see below.

iOS: Local Network permission

Sending to a LAN address triggers the system Local Network prompt, so your app needs NSLocalNetworkUsageDescription in its Info.plist:

<key>NSLocalNetworkUsageDescription</key>
<string>Sends OSC messages to devices on your local network.</string>

Two things to design around:

  • The prompt fires on the first send(), not on bind() or receive().
  • That first datagram may be dropped while the prompt is pending, and every datagram is dropped if the user denies it. send() does not throw in either case — UDP delivery is never acknowledged, so a successful send() means the packet left, not that it arrived.

NSBonjourServices is not required. This library performs no discovery.


Quick start

import { NitroModules } from 'react-native-nitro-modules'
import { encodeOscMessage, OscSocket } from 'react-native-nitro-osc'

const socket = NitroModules.createHybridObject<OscSocket>('OscSocket')
socket.bind(0)                                   // 0 = let the OS pick a port

const bytes = encodeOscMessage({
  address: '/layer/1/opacity',
  args: [{ type: 'f', value: 0.25 }],
})

if (bytes != null) {
  socket.send('192.168.1.42', 9000, bytes)
}

socket.close()

API

OscSocket

A UDP socket, and nothing more. Send and receive share one file descriptor, so this is a single object rather than a sender/receiver pair.

import { NitroModules } from 'react-native-nitro-modules'
import { OscSocket } from 'react-native-nitro-osc'

const socket = NitroModules.createHybridObject<OscSocket>('OscSocket')

| Method | Signature | Description | |---|---|---| | bind | (port: number, host?: string) => number | Binds the local socket and returns the port actually bound. Pass 0 to let the OS assign one. host defaults to the IPv4 wildcard. Throws if already bound, or if the bind fails. | | send | (host: string, port: number, data: ArrayBuffer) => void | Sends one datagram. Requires a prior bind. | | receive | (timeoutMs: number) => ArrayBuffer \| null | Blocks up to timeoutMs for one datagram, returning its bytes or null on timeout. Requires a prior bind. | | close | () => void | Closes the socket. Safe to call more than once. |

bind() first. send() and receive() both throw if the socket is not bound.

host must be a literal IPv4 dotted-quad. No DNS lookup is performed, and an IPv6-formatted address is rejected rather than silently mishandled.

receive() blocks the calling thread. Poll it with a short timeout — tens of milliseconds, not seconds. "Nothing arrived yet" is the common case, and a long timeout freezes the caller for its full duration.

encodeOscMessage

encodeOscMessage(message: OscMessage): ArrayBuffer | null

Encodes one OSC message. Returns null — never throws — if the address is not sendable or a string argument contains an embedded NUL.

decodeOscPacket

decodeOscPacket(bytes: ArrayBuffer): OscDecodeResult

Decodes a packet into a flat list of messages. Bundles are flattened recursively and their timetags ignored, so callers never branch on bundle-vs-message.

Decoding never throws — a malformed datagram off the network is an ordinary outcome, not an exceptional one:

const result = decodeOscPacket(bytes)
if (result.ok) {
  for (const message of result.messages) {
    console.log(message.address, message.args)
  }
} else {
  console.warn('bad packet:', result.error.kind)
}

result.error.kind is 'malformed' (with a reason) or 'bundle-too-deep' (nesting past 16 levels).

isSendableOscAddress

isSendableOscAddress(path: string): boolean

True if path is a literal OSC address safe to send: starts with /, all characters ASCII-graphic, and none of # * , ? [ ] { }. Pattern-matching address expressions are deliberately rejected — this library sends to concrete addresses.


Argument types

type OscArg =
  | { type: 'i'; value: number }        // int32
  | { type: 'f'; value: number }        // float32
  | { type: 's'; value: string }        // string
  | { type: 'b'; value: ArrayBuffer }   // blob
  | { type: 'd'; value: number }        // float64
  | { type: 'bool'; value: boolean }    // 'T' / 'F'
  | { type: 'nil' }                     // 'N'
  | { type: 'unsupported'; tag: string }

| OSC tag | Encode | Decode | Notes | |---|---|---|---| | i f s b | Yes | Yes | The four types OSC 1.0 requires | | d | Yes | Yes | float64 | | T / F | Yes | Yes | Surfaces as { type: 'bool' } | | N | Yes | Yes | Surfaces as { type: 'nil' } | | h I | No | Partial | Width is known and skipped correctly, but no value is surfaced — you get { type: 'unsupported', tag }. h is int64, which a JS number cannot hold losslessly | | [ ], m, r, c | No | No | Rejects the whole message |

The union is tiered deliberately. A decoder must know every argument's byte width to find where the next one begins — even for values it will not hand back — because one unrecognized tag would otherwise silently misalign everything after it.


Receiving

const socket = NitroModules.createHybridObject<OscSocket>('OscSocket')
const port = socket.bind(9000)
console.log('listening on', port)

// Call this from a timer or a background loop — not in a tight synchronous loop.
const bytes = socket.receive(20)     // short timeout: this blocks
if (bytes != null) {
  const result = decodeOscPacket(bytes)
  if (result.ok) {
    // handle result.messages
  }
}

Limitations

  • IPv4 only. IPv6 hosts are rejected rather than silently mishandled.
  • Bundles decode but do not encode. Send one message at a time.
  • No discovery. No mDNS/Bonjour — you supply the host and port.
  • One thread. A socket is not safe for concurrent send and receive from different threads.
  • Pre-1.0 OSC messages without a type-tag string are treated as malformed.

Design: why the codec is TypeScript

OSC needs no vendor SDK and no OS-gatekept API — it is a message format sent over a plain UDP socket. The wire format (padded strings, type-tag-prefixed arguments, 4-byte alignment) is simple enough that a hand-written TypeScript codec is small, and there is no frame-pacing, reconnect, or native-service logic here that would justify bridging to a native implementation.

So the native surface is as thin as it can be: open a socket, send bytes, receive bytes. It exists at all only because React Native's JS runtime has no datagram API — fetch and WebSocket are HTTP/TCP-shaped, not UDP.


Development

See CONTRIBUTING.md for the repository layout, the checks, and the branching convention.