Transports — Node UDP / React Native UDP / Browser helper
JS/TS SDK は API が 1 つ・トランスポート(送信経路)が 3 つです。
connect() の呼び方は同じでも、Node・React Native・ブラウザで内部の送信方法が変わります。
- Node(Electron / サーバー / CLI / クリエイティブコーディング)→ Wi-Fi UDP をデバイスへ直接送る。
- React Native(Android / iOS のスマホアプリ)→ オプションの
react-native-udpで Wi-Fi UDP を端末から直接送る。 スマホはブラウザのようにサンドボックス化されていないため本物の UDP ソケットを 開けるので、hapbeat-helper は不要。 - ブラウザ(WebXR / three.js / p5.js / jsPsych など)→ ローカルで動く
hapbeat-helper に WebSocket(
ws://localhost:7703)で中継する。 ブラウザは生の UDP ソケットを開けないため、helper が代わりに送信する。
どちらを使うかは自分で選ぶ必要はありません。パッケージの exports map が
ランタイム/バンドラーを見て自動で正しいビルドを選びます。
なぜ 3 つのビルドが必要か
Section titled “なぜ 3 つのビルドが必要か”Hapbeat デバイスは LAN 上で UDP を受け、パケット内の target で自己フィルタします
(詳細は Address の仕組み)。Node は node:dgram で、
React Native は react-native-udp で UDP を直接送れますが、
ブラウザのサンドボックスは生 UDP を許可しません。
そのためブラウザ側はローカルの helper デーモンに WebSocket で指示を渡し、helper が
UDP 送信を代行します。
この差を吸収するため、SDK は実体が異なる 3 つのエントリポイントを持ちます。
| ビルド | エントリ | 依存 | 送信経路 |
|---|---|---|---|
| Node | dist/node.js | node:dgram | UDP 直接(既定 unicast) |
| React Native | dist/react-native.js | react-native-udp | UDP 直接(既定 unicast) |
| Browser | dist/browser.js | WebSocket | hapbeat-helper 経由 |
node:dgram がブラウザバンドルに混入しないよう、トランスポート実装は
エントリごとに分離されています。
exports map による自動選択
Section titled “exports map による自動選択”@hapbeat/sdk の package.json は exports 条件で出し分けます。
"exports": { ".": { "node": { "default": "./dist/node.js" }, // Node ランタイム "react-native": { "default": "./dist/react-native.js" }, // React Native ランタイム "browser": { "default": "./dist/browser.js" }, // バンドラーの browser 条件 "default": { "default": "./dist/browser.js" } // それ以外(WebXR 等) }}- Node で実行 →
node条件にマッチ → UDP ビルド。 - React Native(Metro)でバンドル →
react-native条件にマッチ → RN UDP ビルド。 - Vite / webpack / esbuild でバンドル →
browser条件にマッチ → helper ビルド。 - どれにもマッチしないランタイムは
default(= browser ビルド)にフォールバック。
利用側のコードは常に同じです。
import { connect } from "@hapbeat/sdk"; // どちらのビルドかは exports が決めるconst hb = await connect({ appName: "MyApp" });Node — UDP 直接送信
Section titled “Node — UDP 直接送信”Node ビルドは node:dgram で UDP4 ソケットを開き、PLAY / STOP /
CONNECT_STATUS などのパケットを直接送ります。
const hb = await connect({ appName: "MyApp", // OLED 表示名(最大 16 文字) port: 7700, // 既定 7700(コマンド送信先ポート) broadcastAddr: "255.255.255.255", // 既定(フォールバック時の宛先) keepalive: true, // 既定 true unicast: true, // 既定 true(下記) // deviceTtlMs: 15000, // PONG からこの時間だけ unicast 宛先として保持 // bindPort: 7700, // opt-in: well-known 受信ポートを bind(既定は ephemeral)});- 送信先は
7700(デバイスのコマンドポート)。受信ソケットは既定で ephemeral(OS 任せ)ポートを bind します(DEC-036)。well-known の 7700 を bind するのは daemon(hapbeat-helper)だけ、という方針で、SDK が helper から 7700 を奪わないようにするためです。PONG は送信元の ephemeral ポートへ返るので discovery は成立します。 - daemon 的に非要求のブロードキャストを 7700 で受けたい場合は
bindPort: 7700を明示します(7700 が使用中なら ephemeral にフォールバック)。 keepalive(既定 true)は 5 秒間隔で PING を送ります。デバイスが PONG を 返すのは PING に対してだけなので、これが unicast の宛先表を維持します。appNameを設定している場合は併せてCONNECT_STATUSも送り、デバイス OLED に アプリ名を表示します(hb.close()で「アプリが離れた」通知を送って解除します)。
送信は unicast が標準
Section titled “送信は unicast が標準”PLAY / STOP / STOP_ALL とストリームは、PING に応答した既知デバイスへ
unicast されます。まだ 1 台も応答していない間だけブロードキャストにフォール
バックします(二重送信はしません)。
Wi-Fi の AP は、同じ AP に省電力状態の端末が 1 台でもいると group-addressed フレームを次の DTIM ビーコン(100〜300 ms 周期)まで保留します。これが単発 コマンドの発火遅れ、連続ストリームの周期的な途切れとして出ます。デバイス側の 設定では回避できない(原因は無関係な他端末)ため、送信側を unicast にします。
- 多数台を厳密に同時発火させたい場合は
unicast: falseでブロードキャストに 固定できます(1 回の送信で全台に届く。unicast は台数分を順に送る)。 targetに一致しないデバイスは宛先から外れます。全台不一致だった場合、 コマンドはブロードキャストにフォールバックします(デバイス側でも同じ判定を するため誤発火はせず、キャッシュが古いときに STOP が消える方が危険なため)。- 宛先表は上記の keep-alive PING で維持されます。
マルチ NIC(multi-homed)の注意
Section titled “マルチ NIC(multi-homed)の注意”PC が複数のネットワークインターフェイスを持つ場合(有線 + Wi-Fi、VPN、Docker の
仮想 NIC など)、255.255.255.255 宛のブロードキャストが Hapbeat とは別の NIC から
出ていくことがあります。探索の PING はブロードキャストなので、これが起きると
デバイスが 1 台も見つからず、送信も unicast になりません。デバイスが見つからない・
鳴らないときは、Hapbeat と同じ LAN に繋がっている NIC が経路(route)を持っているか
確認してください。
特定セグメントに送りたい場合は broadcastAddr をそのサブネットの
ブロードキャストアドレス(例 192.168.1.255)に指定します。
React Native — UDP 直接送信(helper 不要)
Section titled “React Native — UDP 直接送信(helper 不要)”React Native ビルドは、オプションの peer 依存 react-native-udp を使って
スマホから UDP を直接送ります(Node と同じく既定 unicast)。スマホはブラウザのように
サンドボックス化されていないため本物の UDP ソケットを開けます。よって
hapbeat-helper は不要で、ワイヤーフォーマットは Node と同一です。
exports の react-native 条件が dist/react-native.js を解決します。
const hb = await connect({ appName: "MyApp" });hb.play("sample-kit.sine_100hz", { gain: 0.5 });アプリ側のセットアップ
Section titled “アプリ側のセットアップ”-
依存をインストールします。
npm install react-native-udp fast-text-encodingreact-native-udp… UDP のネイティブモジュール(autolink されます)。fast-text-encoding… 必須の polyfill。RN Hermes(0.86 を含む)はTextEncoderを持ちますがTextDecoderを持たず、ワイヤープロトコルの デコードに必要なためです。
-
metro.config.jsの resolver で@hapbeat/sdkが React Native ビルドへ 解決されるようにします。// metro.config.jsconfig.resolver.unstable_enablePackageExports = true;config.resolver.unstable_conditionNames = ["react-native", "require", "default"]; -
import 'fast-text-encoding';を最初の import にします (@hapbeat/sdkより前・index.jsかApp.tsxの先頭)。順序が後だとReferenceError: Property 'TextDecoder' doesn't existになります。import "fast-text-encoding"; // ← 最初に。@hapbeat/sdk より前import { connect } from "@hapbeat/sdk";
プラットフォームの権限メモ
Section titled “プラットフォームの権限メモ”- Android:
INTERNET権限は既定で付与され、ブロードキャスト送信はそのまま動きます。 探索の PONG 受信はネットワークによって multicast lock が必要な場合があります。 AP / クライアント分離が有効なネットワークではブロードキャストが届きません。 - iOS 14+: ローカルネットワーク権限が必要です
(
Info.plistにNSLocalNetworkUsageDescriptionを追加)。
動作する完全な例は Examples を参照してください。
ブラウザ — hapbeat-helper 経由
Section titled “ブラウザ — hapbeat-helper 経由”ブラウザビルドは UDP を送れないため、ローカルの hapbeat-helper に WebSocket で
指示(play_event / stream_begin など)を渡し、helper が UDP ブロードキャストします。
helper のインストールと起動
Section titled “helper のインストールと起動”pip install hapbeat-helperhapbeat-helper # ws://localhost:7703 で待ち受けconst hb = await connect({ appName: "MyWebXR", helperUrl: "ws://localhost:7703", // 既定 connectTimeoutMs: 4000, // 既定。helper 無応答時に reject するまで onConnectionLost: () => { // 確立後に helper が落ちた/再起動したとき呼ばれる console.warn("hapbeat-helper の接続が切れました"); },});- helper に到達できない/
connectTimeoutMs内に応答が無いとconnect()は reject します。ユーザーにはpip install hapbeat-helperと起動を案内してください。 onConnectionLostは、いったん確立した接続が後から切れた(helper が終了・再起動した) ときのみ呼ばれます。初回接続失敗はconnect()の reject 側で扱います。
トランスポート間の能力差
Section titled “トランスポート間の能力差”play / stop / stopAll(command モード)は 全トランスポートで同じに動きます。
UDP を直接送る Node と React Native は能力が一致し、ブラウザ(helper 経由)の
ストリーミング(clip / live)まわりだけ一部制約があります。
| 機能 | Node(UDP 直接) | React Native(UDP 直接) | Browser(helper WS) |
|---|---|---|---|
command 再生 play(id) | ✅ | ✅ | ✅ |
target 指定(command) | ✅ デバイス側で自己フィルタ | ✅ デバイス側で自己フィルタ | ✅ |
targetTimeUs(同期再生) | ✅ パケットに乗せて送る | ✅ パケットに乗せて送る | ⚠️ 無視(即時再生のみ) |
| clip / live ストリーミング | ✅ | ✅ | ✅ |
| clip / stream の per-device ターゲティング | ✅ パケット内 address で絞る | ✅ パケット内 address で絞る | ⚠️ helper が知る全デバイスへ届く |
| 送信経路 | ✅ 既定 unicast(未検出時のみブロードキャスト) | ✅ 既定 unicast(未検出時のみブロードキャスト) | helper が決定 |
| keep-alive | ✅ PING 5 秒間隔(+ CONNECT_STATUS) | ✅ PING 5 秒間隔(+ CONNECT_STATUS) | — |
デバイス探索 discover() | ✅ ブロードキャスト PING/PONG | ✅ ブロードキャスト PING/PONG | ✅ helper の rescan 経由 |
ブラウザ側の制約の理由:
targetTimeUs無視: helper WS の level-1 プロトコルは予約再生時刻を公開して おらず、即時再生のみを中継します。- clip が全デバイスに届く: helper はストリームのターゲットを既知デバイスの IP へ
解決する設計で、アドレス文字列(
player_1/chest等)による per-device の clip ターゲティングは現状未対応です。Node の clip ストリーミングはパケット内 address を 尊重します。
これらは Command vs Clip / Streaming — clips & ad-hoc PCM / Live Streaming (openStream) にも関連します。
バンドラーと Electron
Section titled “バンドラーと Electron”- Vite / webpack / esbuild は
exportsのbrowser条件を自動で解決するため、 特別な設定は不要です(バンドル時に browser ビルドが選ばれます)。 - Electron は構成次第で使い分けられます。レンダラープロセスでも、Node 統合を 有効にしていれば **node ビルド(UDP 直接)**を使えます。helper を別途立てずに デバイスへ直接送れるため、デスクトップアプリでは node ビルドが扱いやすい選択肢です。
- API は 1 つ、トランスポートは 3 つ。選択は
exportsmap が自動で行う。 - Node = UDP 直接(送信 7700・既定は既知デバイスへ unicast/未検出時のみブロードキャスト・
受信は既定 ephemeral/
bindPortで opt-in・keep-alive あり・マルチ NIC に注意)。 - React Native = UDP 直接(要
react-native-udp+fast-text-encodingpolyfill・metro.config.jsresolver・polyfill は最初の import・helper 不要)。 - Browser = hapbeat-helper 経由(要
pip install hapbeat-helper・targetTimeUsと clip の per-device ターゲティングに制約あり)。 - command モードの挙動は全経路で一致するので、まずは command から始めると差を意識せず済む。
- Getting Started — インストールと最初のイベント
- Command vs Clip — command と clip の使い分け
- Live Streaming (openStream) — 連続ストリーミング(
openStream)