SIP.jsで通話できないときに確認すること|WebSocket・ICE・SIP認証の基本

JavaScript
この記事は約21分で読めます。

この記事の最終更新日: 2026年6月28日

SIP.jsでブラウザ通話を実装していると、「接続できない」「REGISTERは成功するのに通話できない」「発信はできるが音が出ない」「社内ネットワークだと失敗する」といった問題にぶつかることがあります。

このとき大事なのは、いきなりSIP.jsのコードだけを疑わないことです。SIP.jsの通話は、大きく分けると次の3層で成り立っています。

  • SIPサーバーとの接続:WebSocket / WSS
  • SIPとしての認証・登録:URI、認証ユーザー名、パスワード、REGISTER
  • 音声・映像の通信:WebRTC、SDP、ICE、STUN/TURN

つまり「通話できない」と言っても、原因はWebSocket接続、SIP認証、SIPメッセージ、WebRTCメディア通信のどこかに分かれます。この記事では、SIP.jsで通話できないときに確認すべきポイントを、実務で切り分けしやすい順番で整理します。


まず理解したい:SIP.jsの通話は「SIP」と「WebRTC」の組み合わせ

SIP.jsは、ブラウザ上でSIPを扱うためのJavaScriptライブラリです。ただし、ブラウザから従来のSIP UDP/TCPに直接接続するのではなく、基本的にはSIP over WebSocketを使ってSIPサーバーと通信します。SIP over WebSocketでは、WebSocket上でSIPメッセージを送受信するため、WebSocketのハンドシェイクやサブプロトコルの設定が重要になります。IETFの仕様でも、WebSocketはSIPメッセージを運ぶためのトランスポートとして定義されており、Sec-WebSocket-Protocol: sip によってSIP用のWebSocketサブプロトコルをネゴシエーションします。(IETF Datatracker)

一方で、実際の音声・映像データはSIPそのものではなく、WebRTC側で扱われます。SIPは「誰に発信するか」「通話を開始するか」「切断するか」「SDPを交換するか」といったシグナリングを担当し、メディアの疎通はWebRTCのICEやRTPまわりで決まります。SIP over WebSocketの仕様でも、メディア転送はWebSocketトランスポートの範囲外とされています。(IETF Datatracker)

そのため、SIP.jsで通話できない場合は、次の順番で切り分けるのが安全です。

  1. WebSocketが接続できているか
  2. SIP認証・REGISTERが成功しているか
  3. INVITE / 200 OK / ACK などのSIPメッセージが流れているか
  4. SDPの中身が正しいか
  5. ICE Candidateが交換され、メディア経路が確立しているか
  6. マイク権限・音声再生・ブラウザ制約に引っかかっていないか

1. WebSocket接続を確認する

最初に確認すべきは、SIPサーバーへのWebSocket接続です。SIP.jsの問題に見えても、実際には wss://... に接続できていないだけ、というケースはよくあります。

SIP.jsのSimpleUserでは、SIP over WebSocketサーバーのURLを指定してインスタンスを作成し、connect() でサーバーへ接続します。公式ガイドでも、wss://sip.example.com のようなWebSocketサーバーURLを指定する例が示されています。(SIP.js)

import { SimpleUser } from "sip.js/lib/platform/web";

const server = "wss://sip.example.com";

const simpleUser = new SimpleUser(server, {
  aor: "sip:alice@example.com",
  media: {
    remote: {
      audio: document.getElementById("remoteAudio") as HTMLAudioElement,
    },
  },
});

await simpleUser.connect();

ここで確認したいポイントは次の通りです。

確認項目見るべきポイント
WebSocket URLws:// ではなく本番では基本的に wss:// を使っているか
ポートSIP WebSocket用のポートが開いているか
TLS証明書証明書エラーが出ていないか
パス/ws/sip-ws など、SIPサーバー側のWebSocketパスが正しいか
サブプロトコルSIP over WebSocketとして受け付けられているか
CORS / Origin制限サーバー側でブラウザのOriginを拒否していないか
リバースプロキシNginxなどでUpgradeヘッダーが正しく転送されているか

Chrome DevToolsのNetworkタブで、WebSocketの接続が 101 Switching Protocols になっているかを確認します。WebSocketハンドシェイクではHTTP Upgradeが成功すると101応答になり、その後WebSocket接続として利用されます。(IETF Datatracker)

よくある失敗例

WebSocket connection to 'wss://example.com/ws' failed

この場合、SIP認証以前の問題です。SIP IDやパスワードを見直す前に、まずWebSocket URL、TLS、ポート、プロキシ設定を確認します。

特にNginxを挟んでいる場合は、次のような設定が不足しているとWebSocketが失敗します。

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;

WebSocketがつながっていない状態では、REGISTERもINVITEも送れません。まずは「SIPサーバーにWebSocketで到達できているか」を最初に潰すのが重要です。


2. SIP認証・REGISTERを確認する

WebSocket接続が成功したら、次はSIP認証です。

SIP.jsで認証付きUserAgentを作成する場合、uriauthorizationUsernameauthorizationPassword を設定します。SIP.js公式ガイドでも、認証付きUserAgentではこれらを UserAgentOptions に渡し、その後 Registerer を作成して register() する流れが示されています。(SIP.js)

import { UserAgent, Registerer } from "sip.js";

const uri = UserAgent.makeURI("sip:alice@example.com");

if (!uri) {
  throw new Error("Invalid SIP URI");
}

const userAgent = new UserAgent({
  uri,
  authorizationUsername: "alice",
  authorizationPassword: "password",
  transportOptions: {
    server: "wss://sip.example.com/ws",
  },
});

const registerer = new Registerer(userAgent);

await userAgent.start();
await registerer.register();

ここで注意したいのは、uriauthorizationUsername が必ずしも同じとは限らないことです。

例えば、SIPアドレスは次のようになっているとします。

sip:1001@example.com

しかし、認証ユーザー名は次のように別の値になっていることがあります。

authorizationUsername: "1001"

または、サービスによっては次のようにドメイン付きのユーザー名を要求する場合もあります。

authorizationUsername: "1001@example.com"

この違いを間違えると、REGISTERで 401 Unauthorized403 Forbidden が返ってきます。

401 Unauthorized は必ずしも失敗ではない

SIP認証では、最初のREGISTERに対して 401 Unauthorized が返り、その後Digest認証情報を付けて再送する流れがあります。そのため、ログ上に一度 401 が出ているだけで即失敗とは限りません。

見るべきなのは、最終的にREGISTERが成功しているかです。

成功していれば、一般的には次のような流れになります。

REGISTER
401 Unauthorized
REGISTER with Authorization
200 OK

失敗している場合は、次のような状態になります。

REGISTER
401 Unauthorized
REGISTER with Authorization
403 Forbidden

または、

REGISTER
404 Not Found

REGISTERできないときの確認項目

症状主な確認ポイント
401が続くパスワード、realm、認証ユーザー名が違う
403になるアカウントが無効、接続元制限、権限不足
404になるSIP URIのドメインやユーザーが存在しない
408 / timeoutSIPサーバーに到達できない、NAT、ネットワーク問題
423 Interval Too BriefREGISTERのexpires値が短すぎる

SIP.js側だけでなく、SIPサーバー側のログも必ず確認した方がよいです。Asterisk、FreeSWITCH、Kamailio、OpenSIPSなどを使っている場合、ブラウザ側のログだけでは認証失敗の理由が見えにくいことがあります。


3. 発信できない場合はINVITEの流れを見る

REGISTERが成功しているのに発信できない場合は、INVITEの流れを確認します。

SIP.jsのフルAPIでは、発信時に Inviter を作成し、invite() を呼ぶことでSIP INVITEを送信します。公式ガイドでも、UserAgent.makeURI() で宛先URIを作成し、new Inviter(userAgent, target) の後に inviter.invite() を呼ぶ例が示されています。(SIP.js)

import { Inviter, UserAgent } from "sip.js";

const target = UserAgent.makeURI("sip:bob@example.com");

if (!target) {
  throw new Error("Invalid target URI");
}

const inviter = new Inviter(userAgent, target);

await inviter.invite();

発信時に見るべきSIPメッセージの基本形は次の通りです。

INVITE
100 Trying
180 Ringing
200 OK
ACK

この流れのどこで止まっているかによって、原因が変わります。

止まる場所考えられる原因
INVITEが送られないJSエラー、UserAgent未start、宛先URI不正
100 Tryingが返らないSIPサーバー到達不可、ルーティング不備
404 Not Found宛先ユーザーが存在しない、URI不正
480 Temporarily Unavailable相手が未登録、着信不可
486 Busy Here相手が通話中・拒否
488 Not Acceptable HereSDP、コーデック、メディア条件の不一致
200 OK後に音が出ないSIPではなくWebRTC/ICE/音声再生側の問題

特に重要なのは、200 OK が返っているのに音が出ない場合です。この場合、SIPのシグナリング自体は成立している可能性が高く、問題はWebRTCのメディア経路、ICE、ブラウザのマイク・スピーカー制御に移ります。


4. 着信できない場合はREGISTERとonInviteを見る

発信はできるが着信できない場合、まずREGISTERが成功しているかを確認します。SIP.js公式ガイドでは、着信を受けるには登録済みUserAgentを前提とし、UserAgentの delegateonInvite を設定してINVITEを受け取る流れが説明されています。(SIP.js)

const userAgent = new UserAgent({
  uri,
  authorizationUsername: "alice",
  authorizationPassword: "password",
  transportOptions,
  delegate: {
    onInvite: async (invitation) => {
      console.log("Incoming call");
      await invitation.accept();
    },
  },
});

着信できない場合は、次の順番で確認します。

  1. REGISTERが成功しているか
  2. SIPサーバー上で該当ユーザーが登録済みになっているか
  3. 着信INVITEがブラウザまで届いているか
  4. delegate.onInvite が設定されているか
  5. invitation.accept() または reject() の処理が正しく呼ばれているか
  6. ブラウザがスリープ、タブ破棄、ネットワーク切断状態になっていないか

Webアプリの場合、ネイティブアプリと違って常時バックグラウンドで安定動作するとは限りません。着信待ちを本格的にやるなら、ブラウザのライフサイクル、再接続、REGISTER更新、ユーザーの画面状態まで考慮する必要があります。


5. WebSocketは成功しているのに音が出ない場合はICEを見る

SIP.jsで一番ハマりやすいのが、「SIP上は通話成立しているのに音が出ない」パターンです。

この場合、SIP.jsのコードよりもWebRTCのICEを疑います。ICE Candidateは、WebRTCが相手と通信するための経路情報です。MDNでは、ICE CandidateはWebRTCがリモート端末と通信するために必要なプロトコルや経路を表す情報であり、複数候補の中から最終的に使う経路が選ばれると説明されています。(MDNウェブドキュメント)

WebRTCでは、次のような候補が出てきます。

Candidate種別意味
host端末のローカルIPなど、直接到達できる候補
srflxSTUNサーバー経由で見えるグローバル側の候補
relayTURNサーバーを経由する候補

MDNのWebRTC接続解説でも、ICE Candidateは直接通信またはTURNサーバー経由の通信方法を示すもので、通常は候補を順に提案しながら最適な接続経路を選ぶと説明されています。(MDNウェブドキュメント)

STUNだけでは足りないケースがある

ローカル環境や同一ネットワークでは通話できるのに、別拠点、モバイル回線、企業ネットワーク、VPN越しだと音が出ない場合があります。

この場合、STUNだけではNATを越えられず、TURNサーバーが必要になることがあります。

const userAgent = new UserAgent({
  uri,
  authorizationUsername: "alice",
  authorizationPassword: "password",
  transportOptions: {
    server: "wss://sip.example.com/ws",
  },
  sessionDescriptionHandlerFactoryOptions: {
    peerConnectionConfiguration: {
      iceServers: [
        { urls: "stun:stun.example.com:3478" },
        {
          urls: "turn:turn.example.com:3478",
          username: "turn-user",
          credential: "turn-password",
        },
      ],
    },
  },
});

SIP上では 200 OKACK まで完了しているのに無音の場合、次を確認します。

  • ICE Candidateが生成されているか
  • relay candidateが出ているか
  • TURNサーバーに接続できているか
  • UDPがブロックされていないか
  • 片方向音声ではないか
  • SDP内のIPアドレスがプライベートIPのままになっていないか
  • SIPサーバー、メディアサーバー、TURNサーバーのネットワーク経路が正しいか

Chromeなら chrome://webrtc-internals を使うと、ICE Candidate、selected candidate pair、パケット送受信、audio levelなどを確認できます。


6. マイク権限・HTTPS・ブラウザ制約を確認する

SIP.jsの設定が正しくても、ブラウザ側でマイクが使えなければ通話はできません。

WebRTCでマイクやカメラを使う場合、ブラウザは getUserMedia() によってユーザーに権限を求めます。MDNによると、getUserMedia() はマイクやカメラなどのメディア入力へのアクセス許可をユーザーに求め、許可されると MediaStream を返します。また、このAPIはHTTPSなどのセキュアコンテキストでのみ利用できます。(MDNウェブドキュメント)

よくある失敗は次の通りです。

症状原因
マイク許可ダイアログが出ないHTTPSではない、iframe権限がない、ブラウザ設定で拒否済み
NotAllowedErrorユーザーが拒否、ブラウザ設定で拒否、権限ポリシーで拒否
NotFoundErrorマイクデバイスが見つからない
NotReadableError他アプリがマイクを掴んでいる、OS側の問題
ローカルでは動くが本番で失敗HTTPS、Permissions-Policy、iframe設定の違い

特に埋め込みiframe内で通話UIを動かしている場合は注意が必要です。iframeallow="camera; microphone" がないと、マイク権限が正しく要求できないことがあります。

<iframe
  src="https://example.com/softphone"
  allow="camera; microphone"
></iframe>


7. 通話は成立しているのに相手の音が聞こえない場合

SIPもICEも成功しているように見えるのに、相手の音が聞こえない場合は、メディア要素への接続と再生制御を確認します。

SIP.js公式ガイドでは、セッションが Established になったタイミングで sessionDescriptionHandler.peerConnection.getReceivers() からトラックを取得し、MediaStream に追加してHTMLのメディア要素へ設定する例が示されています。(SIP.js)

const remoteAudio = document.getElementById("remoteAudio") as HTMLAudioElement;

function setupRemoteMedia(session: any) {
  const remoteStream = new MediaStream();

  session.sessionDescriptionHandler.peerConnection
    .getReceivers()
    .forEach((receiver: RTCRtpReceiver) => {
      if (receiver.track) {
        remoteStream.addTrack(receiver.track);
      }
    });

  remoteAudio.srcObject = remoteStream;
  remoteAudio.play().catch((error) => {
    console.error("音声の再生に失敗しました", error);
  });
}

ここで重要なのが、play() は失敗する可能性があるという点です。MDNでは、HTMLMediaElement.play() は再生開始を試みるPromiseを返し、ブラウザやOSが現在の状況で再生を許可しない場合は NotAllowedError で拒否されることがあると説明されています。(MDNウェブドキュメント)

つまり、通話自体は成立していても、ブラウザの自動再生制限によって音が出ないことがあります。

対策としては、次のようにユーザー操作を起点に通話開始や音声再生を行います。

<button id="callButton">通話開始</button>
<audio id="remoteAudio" autoplay></audio>

document.getElementById("callButton")?.addEventListener("click", async () => {
  await startCall();
});

自動着信や自動応答を実装する場合でも、最初にユーザーが「開始」ボタンを押す設計にしておくと、ブラウザの再生制限に引っかかりにくくなります。


8. SDPとコーデックを確認する

SIPメッセージのやり取りが成功していても、SDPの内容が合わないと通話できません。

特に確認したいのは次の項目です。

  • m=audio が存在するか
  • a=sendrecv / sendonly / recvonly / inactive が意図通りか
  • 対応コーデックが一致しているか
  • Opus、PCMU、PCMAなどの扱い
  • c= 行のIPアドレスが正しいか
  • RTP/SAVPFなどWebRTC前提の形式になっているか
  • DTLS-SRTPのfingerprintが含まれているか

ブラウザのWebRTCは、通常のSIP電話機とは前提が違います。SIPサーバーやPBX側がWebRTCに対応していない場合、SIP.jsからINVITEを送っても 488 Not Acceptable Here になることがあります。

この場合は、SIP.jsのコード修正ではなく、PBX側でWebRTC対応、DTLS-SRTP、ICE、コーデック、WebSocket transportを有効にする必要があります。


9. 通話終了処理も状態ごとに分ける

通話開始だけでなく、終了処理も確認が必要です。

SIP.jsでは、セッション状態によって終了時に呼ぶべきメソッドが変わります。公式ガイドでは、未確立の発信セッションは cancel()、未確立の着信セッションは reject()、確立済みセッションは bye() を使うと説明されています。(SIP.js)

import { Inviter, SessionState } from "sip.js";

function endCall(session: any) {
  switch (session.state) {
    case SessionState.Initial:
    case SessionState.Establishing:
      if (session instanceof Inviter) {
        session.cancel();
      } else {
        session.reject();
      }
      break;

    case SessionState.Established:
      session.bye();
      break;

    case SessionState.Terminating:
    case SessionState.Terminated:
      break;
  }
}

終了処理が不適切だと、次回発信時に古いセッションやメディアトラックが残って不安定になることがあります。

通話終了時には、次の処理も忘れずに行います。

function cleanupMedia() {
  const remoteAudio = document.getElementById("remoteAudio") as HTMLAudioElement;

  if (remoteAudio.srcObject instanceof MediaStream) {
    remoteAudio.srcObject.getTracks().forEach((track) => track.stop());
  }

  remoteAudio.srcObject = null;
  remoteAudio.pause();
}


10. 実務で使える切り分けチェックリスト

最後に、SIP.jsで通話できないときのチェックリストをまとめます。

WebSocketまわり

  • wss:// のURLが正しいか
  • WebSocket接続が 101 Switching Protocols になっているか
  • TLS証明書エラーがないか
  • NginxなどでUpgradeヘッダーが転送されているか
  • SIP over WebSocket対応のエンドポイントに接続しているか
  • サーバー側でOrigin制限に引っかかっていないか

SIP認証まわり

  • uri が正しいか
  • authorizationUsername が正しいか
  • authorizationPassword が正しいか
  • REGISTERが最終的に 200 OK になっているか
  • SIPサーバー上でユーザーが登録済みになっているか
  • realmやドメインが一致しているか

発信まわり

  • 宛先URIが正しいか
  • Inviter が作成できているか
  • INVITEが送信されているか
  • 100 Trying180 Ringing200 OKACK のどこで止まっているか
  • 404480486488 などのレスポンスコードを確認したか

着信まわり

  • REGISTERが成功しているか
  • SIPサーバーからINVITEが届いているか
  • delegate.onInvite を設定しているか
  • invitation.accept() が呼ばれているか
  • ブラウザタブやネットワークが維持されているか

WebRTC / ICEまわり

  • ICE Candidateが生成されているか
  • STUNサーバーを設定しているか
  • TURNサーバーが必要なネットワークではないか
  • relay candidateが出ているか
  • chrome://webrtc-internals でselected candidate pairを確認したか
  • 片方向音声になっていないか
  • UDPがブロックされていないか

メディア・ブラウザまわり

  • HTTPSで配信しているか
  • マイク権限が許可されているか
  • iframeの場合、allow="camera; microphone" があるか
  • remoteAudio.srcObject にMediaStreamを設定しているか
  • audio.play() のPromise rejectionを処理しているか
  • 自動再生制限に引っかかっていないか

まとめ

SIP.jsで通話できないときは、「SIP.jsが悪い」と決めつける前に、層ごとに切り分けることが大切です。

特に重要なのは、次の3つです。

  1. WebSocketがつながっているか
  2. SIP認証・REGISTERが成功しているか
  3. WebRTCのICEでメディア経路が確立しているか

WebSocketが失敗していればSIPメッセージは送れません。REGISTERが失敗していれば着信はできません。SIPのINVITEが成功していても、ICEやTURNが不十分なら音声は流れません。

SIP.jsの通話トラブルは、一見すると複雑に見えます。しかし、WebSocket、SIP認証、INVITE、SDP、ICE、ブラウザ権限の順番で確認すれば、原因はかなり絞り込めます。

実務では、ブラウザのDevTools、SIPサーバーのログ、chrome://webrtc-internals、SIPメッセージログを組み合わせて確認するのが基本です。SIP.jsのコードだけを見るのではなく、シグナリングとメディアの両方を分けて調査することが、安定したブラウザ通話実装への近道です。

コメント

タイトルとURLをコピーしました