
Amazon Chime SDK の通話に同じ会員が 2 台目の端末から入ると、1 台目がどうなるかを参加者の作り方ごとに確認してみた
はじめに
テクニカルサポートの 片方 です。
1 対 1 のビデオ通話のある Web サービスでは、会員がスマートフォンで通話に入った後に、パソコンからも同じ通話を開くことがあります。このとき 1 台目の端末がどうなるかは、サーバーが参加者(attendee)をどう作るかによります。1 台目が 2 台目に押し出されて切れる場合もあれば、1 対 1 の通話に同じ会員が 2 人いるように見える場合もあります。
Amazon Chime SDK では、サーバーが CreateAttendee で参加者を作り、返ってきた JoinToken をブラウザに渡します。JoinToken は、クライアントがその参加者として認証し、会議に入るための値です。ExternalUserId は、自分のサービスの会員 ID などを、参加者に結び付けるための値です。
本ブログでは、2 台目の参加者の作り方を 3 通り試し、3 つの端末それぞれで何が起きるかを確かめました。
構成
| 項目 | 内容 |
|---|---|
| リージョン | API の呼び出し先と、会議のメディアのリージョン(CreateMeeting の MediaRegion)は、どちらも東京リージョン(ap-northeast-1) |
| 会議 | CreateMeeting で、参加者の上限(MeetingFeatures の Attendee の MaxCount)を指定せずに作成。アプリの側で 1 対 1 の通話として使う |
| 端末 | 相手(member-b)、会員 a の 1 台目と 2 台目(どちらも member-a。per-device では member-a#phone と member-a#pc)の 3 つ |
| ブラウザ | Playwright で Chromium を実行。カメラとマイクは Chromium の疑似デバイス |
| ライブラリ | amazon-chime-sdk-js 3.32.0、@aws-sdk/client-chime-sdk-meetings 3.1152.0 |
| 再試行 | AWS SDK for JavaScript の standard モード(指数バックオフとジッター)、maxAttempts は 5(初回を含めて 5 回) |

図の番号の順に、相手(member-b)が先に入り、続いて会員 a(member-a)が 1 台目の端末で入ります。
その 6 秒後に、サーバー役が 2 台目のための参加者を作り、2 台目が入ります。参加者の作り方は、次の 3 通りです。
- same:1 台目と同じ ExternalUserId(member-a)で CreateAttendee を呼ぶ
- per-device:ExternalUserId を端末ごとに変える(member-a#phone と member-a#pc)
- replace:1 台目の参加者を DeleteAttendee で消してから、同じ ExternalUserId で作り直す
2 台目が入ってから約 10 秒後に、3 つのブラウザで起きた出来事と、ListAttendees の結果を記録しました。
先に結論から
- same では、CreateAttendee から 1 台目と同じ AttendeeId と JoinToken が返りました。2 台目が入ると、1 台目は AudioJoinedFromAnotherDevice で止まりました。相手のブラウザに会員 a が去ったと記録されたのは、7 回中 1 回でした。
- per-device では、2 台とも通話に残り、1 対 1 の会議に参加者が 3 人になりました。
- replace では、AttendeeId は 1 台目と同じで、JoinToken だけが変わりました。1 台目は SignalingBadRequest で止まり、相手のブラウザには、会員 a が一度去ってまた来たと記録されました。
- 消した参加者に DeleteAttendee を呼び直すと、1 回目の 60 秒後まで、4 回とも成功が返りました。
やってみた
次の順に進めました。
- 2 台目の参加者を作る処理を書く
- 3 通りの作り方を 1 回ずつ実行する
- 消した参加者に DeleteAttendee を呼び直す
- same で止まった 1 台目を、同じ JoinToken で入り直させる
- same をさらに 5 回実行する
1. 2 台目の参加者を作る処理を書く
2 台目の参加者を作る部分は、次のとおりです。会議を作る部分と、ブラウザの側は省略しています。
joinInfoFor は、会員の ID を ExternalUserId にして CreateAttendee を呼ぶ関数です。1 台目の参加者は、per-device のときは member-a#phone、それ以外は member-a で作っています。say は、+秒を付けてログを 1 行書く関数です。
const chime = new ChimeSDKMeetingsClient({ region: 'ap-northeast-1', maxAttempts: 5, retryMode: 'standard' });
// …(省略)…
let secondInfo;
if (mode === 'replace') {
await chime.send(new DeleteAttendeeCommand({ MeetingId: meeting.MeetingId, AttendeeId: firstInfo.Attendee.AttendeeId }));
say('1 台目の参加者を DeleteAttendee で消した');
secondInfo = await joinInfoFor(meeting, 'member-a');
} else {
secondInfo = await joinInfoFor(meeting, mode === 'per-device' ? 'member-a#pc' : 'member-a');
}
CreateAttendee の ExternalUserId は冪等性のトークンなので、同じ ExternalUserId で再試行しても、参加者は増えません。
replace は、削除と作り直しの 2 回の呼び出しに分かれます。削除が成功して作り直しが失敗すると、会員 a の参加者がなくなります。その場合は 2 台目に失敗を返し、replace を最初からやり直せるようにします。
ブラウザの側では、Amazon Chime SDK for JavaScript の次の 2 つで出来事を記録しました。
- audioVideoDidStop:通話が止まったときに MeetingSessionStatus を受け取ります。
statusCode()で、止まった理由のコードを取れます。 - realtimeSubscribeToAttendeeIdPresence:参加者が来たときと去ったときに、ExternalUserId と在室かどうかを受け取ります。
2. 3 通りの作り方を 1 回ずつ実行する
3 通りを、次のコマンドで 1 回ずつ実行しました。
node y03.mjs same
node y03.mjs per-device
node y03.mjs replace
3. 消した参加者に DeleteAttendee を呼び直す
会議と参加者を 1 人作って DeleteAttendee を呼び、その直後・10 秒後・60 秒後に、同じ参加者へ DeleteAttendee を呼び直しました。
timeout 170 node y03-delete-attendee.mjs > y03-delete-attendee.log 2>&1
4. same で止まった 1 台目を、同じ JoinToken で入り直させる
same と同じ方法で 2 台目を入れ、2 台目が入ってから 10 秒後に、出来事と ListAttendees を記録しました。続けて、止まった 1 台目のページで、同じ JoinToken を使って joinMeeting をもう一度呼びました。その 10 秒後に、もう一度出来事と ListAttendees を記録しました。
timeout 170 node y03-rejoin.mjs > y03-rejoin.log 2>&1
5. same をさらに 5 回実行する
手順 4 の実行では、相手のブラウザに、会員 a が去った(member-a:false)と記録されました。手順 2 の same では記録されていなかったので、same だけを y03-same-5.mjs でさらに 5 回実行しました。
確認してみた
ログの +秒は、スクリプトを始めてからの秒数です。「1 台目が入った」の行は、1 台目が入ってから 6 秒待った後に書いているので、入った時刻そのものではありません。
same:同じ ExternalUserId で作る

2 回目の CreateAttendee では、新しい参加者は作られず、1 台目と同じ参加者が返りました。CreateAttendee の ExternalUserId は、冪等性のトークンです。
The Amazon Chime SDK external user ID. An idempotency token.
2 台目が同じ JoinToken で入ると、1 台目の通話は AudioJoinedFromAnotherDevice で止まりました。AudioJoinedFromAnotherDevice は、同じ認証情報で別の端末から入ったときのコードです。
The attendee joined from another device (e.g. another browser window/tab or on mobile) using the same credentials.
相手のブラウザには、会員 a が来た(member-a:true)と 2 回記録され、去ったという記録はありませんでした。ListAttendees は、member-a と member-b の 2 人でした。
手順 4 で、止まった 1 台目を同じ JoinToken で入り直させたときのログは、次のとおりです。
(+2s) 会議を作った(MeetingId 03e8…)
(+8s) 1 台目が入った(AttendeeId 0e94…)
(+9s) 2 台目用に CreateAttendee(AttendeeId 0e94…、1 台目と同じ、JoinToken は同じ)
(+19s) --- 2 台目が入って 10 秒後 ---
(+19s) 1 台目: ["join-called","presence(member-a:true)","presence(member-b:true)","started","stopped(AudioJoinedFromAnotherDevice)"]
(+19s) 2 台目: ["join-called","presence(member-a:true)","presence(member-b:true)","started"]
(+19s) 相手: ["join-called","presence(member-b:true)","started","presence(member-a:true)","presence(member-a:false)","presence(member-a:true)"]
(+21s) ListAttendees: member-a(0e94…), member-b(d2b0…)
(+21s) 1 台目で joinMeeting を同じ JoinToken でもう一度呼んだ
(+31s) --- 1 台目が入り直して 10 秒後 ---
(+31s) 1 台目: ["join-called","presence(member-a:true)","presence(member-b:true)","started","stopped(AudioJoinedFromAnotherDevice)","join-called","presence(member-a:true)","presence(member-b:true)","started"]
(+31s) 2 台目: ["join-called","presence(member-a:true)","presence(member-b:true)","started","stopped(AudioJoinedFromAnotherDevice)"]
(+31s) 相手: ["join-called","presence(member-b:true)","started","presence(member-a:true)","presence(member-a:false)","presence(member-a:true)","presence(member-a:true)"]
(+31s) ListAttendees: member-a(0e94…), member-b(d2b0…)
(+31s) DeleteMeeting: deleted
(+31s) GetMeeting: NotFoundException(会議なし)
1 台目は通話に戻り、今度は 2 台目が AudioJoinedFromAnotherDevice で止まりました。ListAttendees は、入り直しの前も後も、member-a と member-b の 2 人でした。
この実行では、+19s の相手の行に member-a:false が記録されています。手順 5 で same をさらに 5 回実行したときの、相手の行は次のとおりです。5 回とも member-a:false はありませんでした。
(+24s) 相手: ["presence(member-b:true)","started","presence(member-a:true)","presence(member-a:true)"]
(+19s) 相手: ["presence(member-b:true)","started","presence(member-a:true)","presence(member-a:true)"]
(+18s) 相手: ["presence(member-b:true)","started","presence(member-a:true)","presence(member-a:true)"]
(+18s) 相手: ["presence(member-b:true)","started","presence(member-a:true)","presence(member-a:true)"]
(+18s) 相手: ["presence(member-b:true)","started","presence(member-a:true)","presence(member-a:true)"]
same は、手順 2 の 1 回、手順 4 の 1 回、手順 5 の 5 回で、計 7 回実行しました。member-a:false が記録されたのは、そのうち 1 回です。1 台目が止まったことと、ListAttendees が 2 人だったことは、7 回とも同じでした。
per-device:端末ごとに ExternalUserId を変える
+8s 2 台目用に CreateAttendee(AttendeeId 99e2…、1 台目と違う、JoinToken は違う)
+19s 1 台目: ["presence(member-a#phone:true)","presence(member-b:true)","started","presence(member-a#pc:true)"]
+19s 2 台目: ["presence(member-a#pc:true)","presence(member-b:true)","presence(member-a#phone:true)","started"]
+19s 相手: ["presence(member-b:true)","started","presence(member-a#phone:true)","presence(member-a#pc:true)"]
+20s ListAttendees: member-a#pc, member-a#phone, member-b
2 台目は別の参加者として作られ、1 台目は止まりませんでした。1 対 1 で使う会議に参加者が 3 人いて、相手からは、会員 a が 2 人の参加者として見えています。
replace:1 台目を消してから作り直す

作り直した参加者の AttendeeId は 1 台目と同じで、JoinToken は違う値でした。DeleteAttendee のページの記述では、DeleteAttendee は参加者を削除し、その JoinToken も削除します。
1 台目は、SignalingBadRequest で止まりました。SignalingBadRequest は、DeleteAttendee で参加者が削除されたために、シグナリングの接続を確立できなかったときのコードです。
The Amazon Chime SDK for JavaScript failed to establish a signaling connection because you or someone else deleted the attendee using the DeleteAttendee API action in your server application.
MeetingSessionStatusCode には、DeleteAttendee で参加者が削除されて通話が終わったときのコード(AudioAttendeeRemoved)もあります。今回の 1 台目が受け取ったのは、SignalingBadRequest でした。アプリでは、どちらのコードも「この参加者の情報では、もう入れない」として扱うのがよいと判断しました。
相手のブラウザには、会員 a が去った(member-a:false)後に、また来た(member-a:true)と記録されました。
消した参加者にもう一度 DeleteAttendee を呼ぶ
手順 3 のログは、次のとおりです。1 回目の直後・10 秒後・60 秒後に呼び直すと、4 回とも成功が返りました。
(+2s) 会議を作った(MeetingId d252…)
(+2s) 参加者を作った(AttendeeId 654b…)
(+3s) DeleteAttendee 1 回目: 成功
(+3s) DeleteAttendee 2 回目(1 回目の直後): 成功
(+12s) DeleteAttendee 3 回目(1 回目の 10 秒後): 成功
(+62s) DeleteAttendee 4 回目(1 回目の 60 秒後): 成功
(+62s) DeleteMeeting: deleted
(+63s) GetMeeting: NotFoundException(会議なし)
1 回目の 60 秒後までは、DeleteAttendee が 2 回以上呼ばれても、エラーにはなりませんでした。ただし、返ってきた結果からは、参加者がすでに消えているかどうかは見分けられません。
DeleteAttendee のページの記述では、要求の中のリソースが存在しないときは、NotFoundException(404)が返ります。本番では、成功と NotFoundException のどちらも「削除済み」として扱い、作り直しに進むようにします。
replace では、作り直した参加者の AttendeeId が、消した参加者と同じでした。DeleteAttendee は MeetingId と AttendeeId で対象を決めるので、作り直しの後に遅れて DeleteAttendee が届くと、作り直した参加者が消えると考えられます。DeleteAttendee の再試行は、作り直しの前に終わらせます。記事のコードは、DeleteAttendee の完了を await で待ってから作り直しています。
まとめ
1 対 1 の通話で、会員が端末を持ち替える場面に限れば、same の作り方で足りると判断しました。参加者は 2 人のままで、1 台目には AudioJoinedFromAnotherDevice が届くので、「別の端末で通話に入りました」と表示できます。
ただし、same は 1 つの JoinToken を 2 台の端末で使うので、Attendee のページの勧め(JoinToken は、その参加者として認められた 1 つのクライアントだけに渡す)からは外れます。実際に、止まった 1 台目が同じ JoinToken で入り直すと、今度は 2 台目が止まりました。AudioJoinedFromAnotherDevice を受け取った端末は、自動で入り直さないようにしておく必要があります。
1 台目の JoinToken を確実に使えなくしたい場合は replace ですが、相手からは会員 a が一度去ったように見えます。2 台を同時に使わせたい場合は per-device ですが、相手からは会員 a が 2 人に見えます。今回試したのは、3 通りを 1 回ずつ(same は計 7 回)、ブラウザ 3 つで実行した範囲です。
本ブログが誰かの参考になれば幸いです。
参考資料
- CreateAttendee - Amazon Chime SDK
- DeleteAttendee - Amazon Chime SDK
- Attendee - Amazon Chime SDK
- MeetingSessionStatusCode | amazon-chime-sdk-js
クラスメソッドオペレーションズ株式会社について
クラスメソッドグループのオペレーション企業です。
運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AIをフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026年1月 アノテーション㈱から社名変更しました









