コンテンツにスキップ
JA

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-helperWebSocketws://localhost:7703)で中継する。 ブラウザは生の UDP ソケットを開けないため、helper が代わりに送信する。

どちらを使うかは自分で選ぶ必要はありません。パッケージの exports map が ランタイム/バンドラーを見て自動で正しいビルドを選びます。

Hapbeat デバイスは LAN 上で UDP を受け、パケット内の target で自己フィルタします (詳細は Address の仕組み)。Node は node:dgram で、 React Native は react-native-udp で UDP を直接送れますが、 ブラウザのサンドボックスは生 UDP を許可しません。 そのためブラウザ側はローカルの helper デーモンに WebSocket で指示を渡し、helper が UDP 送信を代行します。

この差を吸収するため、SDK は実体が異なる 3 つのエントリポイントを持ちます。

ビルドエントリ依存送信経路
Nodedist/node.jsnode:dgramUDP 直接(既定 unicast)
React Nativedist/react-native.jsreact-native-udpUDP 直接(既定 unicast)
Browserdist/browser.jsWebSockethapbeat-helper 経由

node:dgram がブラウザバンドルに混入しないよう、トランスポート実装は エントリごとに分離されています。

@hapbeat/sdkpackage.jsonexports 条件で出し分けます。

"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 ビルドは 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() で「アプリが離れた」通知を送って解除します)。

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 で維持されます。

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 と同一です。 exportsreact-native 条件が dist/react-native.js を解決します。

const hb = await connect({ appName: "MyApp" });
hb.play("sample-kit.sine_100hz", { gain: 0.5 });
  1. 依存をインストールします。

    npm install react-native-udp fast-text-encoding
    • react-native-udp … UDP のネイティブモジュール(autolink されます)。
    • fast-text-encoding必須の polyfill。RN Hermes(0.86 を含む)は TextEncoder を持ちますが TextDecoder を持たず、ワイヤープロトコルの デコードに必要なためです。
  2. metro.config.js の resolver で @hapbeat/sdk が React Native ビルドへ 解決されるようにします。

    // metro.config.js
    config.resolver.unstable_enablePackageExports = true;
    config.resolver.unstable_conditionNames = ["react-native", "require", "default"];
  3. import 'fast-text-encoding'; を最初の import にします (@hapbeat/sdk より前・index.jsApp.tsx の先頭)。順序が後だと ReferenceError: Property 'TextDecoder' doesn't exist になります。

    import "fast-text-encoding"; // ← 最初に。@hapbeat/sdk より前
    import { connect } from "@hapbeat/sdk";
  • Android: INTERNET 権限は既定で付与され、ブロードキャスト送信はそのまま動きます。 探索の PONG 受信はネットワークによって multicast lock が必要な場合があります。 AP / クライアント分離が有効なネットワークではブロードキャストが届きません。
  • iOS 14+: ローカルネットワーク権限が必要です (Info.plistNSLocalNetworkUsageDescription を追加)。

動作する完全な例は Examples を参照してください。

ブラウザビルドは UDP を送れないため、ローカルの hapbeat-helper に WebSocket で 指示(play_event / stream_begin など)を渡し、helper が UDP ブロードキャストします。

pip install hapbeat-helper
hapbeat-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 側で扱います。

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) にも関連します。

  • Vite / webpack / esbuildexportsbrowser 条件を自動で解決するため、 特別な設定は不要です(バンドル時に browser ビルドが選ばれます)。
  • Electron は構成次第で使い分けられます。レンダラープロセスでも、Node 統合を 有効にしていれば **node ビルド(UDP 直接)**を使えます。helper を別途立てずに デバイスへ直接送れるため、デスクトップアプリでは node ビルドが扱いやすい選択肢です。
  • API は 1 つ、トランスポートは 3 つ。選択は exports map が自動で行う。
  • Node = UDP 直接(送信 7700・既定は既知デバイスへ unicast/未検出時のみブロードキャスト・ 受信は既定 ephemeral/bindPort で opt-in・keep-alive あり・マルチ NIC に注意)。
  • React Native = UDP 直接(要 react-native-udp + fast-text-encoding polyfill・ metro.config.js resolver・polyfill は最初の import・helper 不要)。
  • Browser = hapbeat-helper 経由(要 pip install hapbeat-helpertargetTimeUs と clip の per-device ターゲティングに制約あり)。
  • command モードの挙動は全経路で一致するので、まずは command から始めると差を意識せず済む。