Amazon Chime SDK の通話に同じ会員が 2 台目の端末から入ると、1 台目がどうなるかを参加者の作り方ごとに確認してみた

Amazon Chime SDK の通話に同じ会員が 2 台目の端末から入ると、1 台目がどうなるかを参加者の作り方ごとに確認してみた

Amazon Chime SDKで1対1のビデオ通話中に複数端末から接続する場合、参加者の作り方によって挙動が大きく変わります。同じExternalUserIdで作る場合、端末ごとに変える場合、削除してから作り直す場合の3通りを実際に試し、それぞれの端末やサーバー側でどのような動作が起こるかを確認しました。
2026.11.09

はじめに

テクニカルサポートの 片方 です。
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 回とも成功が返りました。

やってみた

次の順に進めました。

  1. 2 台目の参加者を作る処理を書く
  2. 3 通りの作り方を 1 回ずつ実行する
  3. 消した参加者に DeleteAttendee を呼び直す
  4. same で止まった 1 台目を、同じ JoinToken で入り直させる
  5. 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 は、冪等性のトークンです。

https://docs.aws.amazon.com/chime-sdk/latest/APIReference/API_meeting-chime_CreateAttendee.html

The Amazon Chime SDK external user ID. An idempotency token.

2 台目が同じ JoinToken で入ると、1 台目の通話は AudioJoinedFromAnotherDevice で止まりました。AudioJoinedFromAnotherDevice は、同じ認証情報で別の端末から入ったときのコードです。

https://aws.github.io/amazon-chime-sdk-js/enums/meetingsessionstatuscode.html

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 で参加者が削除されたために、シグナリングの接続を確立できなかったときのコードです。

https://aws.github.io/amazon-chime-sdk-js/enums/meetingsessionstatuscode.html

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 つで実行した範囲です。
本ブログが誰かの参考になれば幸いです。

参考資料

クラスメソッドオペレーションズ株式会社について

クラスメソッドグループのオペレーション企業です。
運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AIをフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026年1月 アノテーション㈱から社名変更しました

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事