안드로이드 (발신)- > 아이폰 (수신) : 음성통화 > 안드로이드에서 말하는게 안들림

[Problem/Question]
// Detailed description of issue.
안드로이드 (발신)- > 아이폰 (수신) : 음성통화 > 안드로이드에서 말하는게 안들림

// If problem, please fill out the below. If question, please delete.
[SDK Version]
// What version of the SDK are you using?
SendBird Call JavaScript SDK v1.10.20 production

[Reproduction Steps]
(발신측 코드)
$(document).on(‘click’, ‘.BtnAlert_onply’, function () {
const corp_mem_idx = ‘<?=$corp_mem_idx?>’;
const user_mem_idx = ‘<?=$user_mem_idx?>’;
const user_mem_nickname = ‘<?=getQueryItem("member","and mem_idx = '$user_mem_idx'","mem_nickname")?>’;

const APP_ID = '<?=_sendbird_app_id?>';

let USER_ID, NICKNAME, USER_PROFILE;
let CALLEE_ID, CALLEE_NICKNAME, CALLEE_PROFILE;

if(corp_mem_idx == '<?=$_SESSION['MEM_IDX']?>'){
  USER_ID = '<?=$corp_mem_idx?>';
  NICKNAME = '<?=$chat_crt_mem_nickname?>';
  USER_PROFILE = '<?=$chat_profile_img?>';

  CALLEE_ID = '<?=$user_mem_idx?>';
  CALLEE_NICKNAME = '<?=$chat_user_mem_nickname?>';
  CALLEE_PROFILE = '<?=$chat_user_img?>';

}else{
  USER_ID = '<?=$_SESSION['MEM_IDX']?>';
  NICKNAME = '<?=$_SESSION['MEM_NICKNAME']?>';
  USER_PROFILE = '<?=$chat_user_img?>';

  CALLEE_ID = '<?=$corp_mem_idx?>';
  CALLEE_NICKNAME = '<?=$chat_crt_mem_nickname?>';
  CALLEE_PROFILE = '<?=$chat_profile_img?>';
}

const selectedRadio = $('input[name="onply_type"]:checked');
const selectedCallType = selectedRadio.val(); // 'call' 또는 'video'
const isVideoCall = selectedCallType === 'video';

const voice_yn = selectedRadio.data('voice_yn');
const video_yn = selectedRadio.data('video_yn'); 

const price = parseInt(selectedRadio.data(isVideoCall ? 'video_price' : 'voice_price'));
const time = parseInt(selectedRadio.data(isVideoCall ? 'video_time' : 'voice_time'));


if (selectedCallType === 'call' && voice_yn === 'N') {
  comAlertMsgBox("현재 음성온플이 불가능합니다.");
  return;
}

if (selectedCallType === 'video' && video_yn === 'N') {
		comAlertMsgBox("현재 영상온플이 불가능합니다.");
  return;
}

if(corp_mem_idx != user_mem_idx){
  const mem_point = parseInt('<?=$mem_point_onply?>');

  if(mem_point < price){
    if(user_mem_idx == '<?=$_SESSION['MEM_IDX']?>'){
      comAlertMsgBox("보유한 코인이 부족합니다.");
    }else{
      comAlertMsgBox("상대방이 보유한 코인이 부족합니다.");
    }
    return;
  }
} 

document.getElementById('onply_btn').click(); 

// 1. 샌드버드 SDK 먼저 초기화해야함.
SendBirdCall.init(APP_ID);

// 2. 샌드버드에 아이디를 등록해야함.
const registerUser = (user_id, nickname, profile) => {
  return fetch('/front/Message/Sendbird', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ user_id, nickname, profile })
  }).then(response => {
    if (!response.ok) throw new Error('등록 실패: ' + response.status);
    return response.json();
  });
};

Promise.all([
  registerUser(USER_ID, NICKNAME, USER_PROFILE),
  registerUser(CALLEE_ID, CALLEE_NICKNAME, CALLEE_PROFILE)
])
  .then(() => SendBirdCall.authenticate({ userId: USER_ID }))
  .then(() => SendBirdCall.connectWebSocket()) // 웹소켓 연결 꼭해야함.
  .then(() => {
    console.log('인증 및 WebSocket 연결 성공');

    const pushData = new FormData();
    const name = '<?=$name?>';
    const corp_fcm_token = '<?=$corp_fcm_token?>';
    const user_fcm_token = '<?=$user_fcm_token?>';

    if(corp_mem_idx != name){
      pushData.append('fcm_token','<?=$corp_fcm_token?>')
      pushData.append('mem_idx', corp_mem_idx);
      pushData.append('crt_idx', user_mem_idx);
    }else{
      pushData.append('fcm_token','<?=$user_fcm_token?>')
      pushData.append('mem_idx',  user_mem_idx);
      pushData.append('crt_idx',  corp_mem_idx);
    }

    pushData.append('onply_type', isVideoCall ? 'video' : 'audio');

    fetch('/front/Message/FcmPush/PB006', {
      method: 'POST',
      cache: 'no-cache',
      body: pushData
    })
    .then((response) => response.json())
    .then((data) => {
      console.log("data", data);
    });

    $('.PopApplyOnply').removeClass('active');
    $('body').removeClass('scroll_lock');
    $('.PopApplyOnply2').addClass('active');
    $('body').addClass('scroll_lock');

    $('.call_waiting_ui').show();
    $('.video_container').hide();
    const localView = document.getElementById('local_video_element_id2');
    const remoteView = document.getElementById('remote_video_element_id2');

    if (isVideoCall) {
      $('.person').css('visibility', 'visible');
      $('#local_video_element_id2').css({
        visibility: 'visible',
      });
      $('#remote_video_element_id2').css({
        visibility: 'visible',
      });
    } else {
      $('.person').css('visibility', 'hidden');
      $('.video_container').css({
        height: '200px'
      });
      $('#local_video_element_id2').css({
        visibility: 'hidden',
        height: '1px'
      });
      $('#remote_video_element_id2').css({
        visibility: 'hidden',
        height: '1px'
      });
    }

    if (!localView || !remoteView) {
      alert('비디오 요소가 로드되지 않았습니다. 다시 시도해주세요.');
      console.error('localView 또는 remoteView가 null입니다:', { localView, remoteView });
      return;
    }

    navigator.mediaDevices.getUserMedia({  video: {width: { ideal: 1280 },height: { ideal: 720 },facingMode: 'user'}, audio: true })
      .then(stream => {
        localView.srcObject = stream;

        let customItems = {
          room: "<?=$room?>",
          user_mem_idx: "<?=$user_mem_idx?>",
          corp_mem_idx: "<?=$corp_mem_idx?>"
        };

        if (isVideoCall) {
          customItems.price = "<?=$crt_video_price?>";
          customItems.time = "<?=$crt_video_time?>";
        } else {
          customItems.price = "<?=$crt_voice_price?>";
          customItems.time = "<?=$crt_voice_time?>";
        }

        const dialParams = {
          userId: CALLEE_ID,
          isVideoCall: isVideoCall,
          callOption: {
            localMediaStream: stream,
            localMediaView: localView,
            remoteMediaView: remoteView,
            audioEnabled: true,
            videoEnabled: isVideoCall
          },
          customItems: customItems
        };

        let isCallConnected = false;
        let callTimeout = null;

        SendBirdCall.dial(dialParams, function (call, error) {
          if (error) {
            console.error('통화 연결 실패:', error);
            comAlertMsgBox('통화 연결에 실패했습니다.\n' + error.message);
            return;
          }

          console.log('통화 요청 전송:', call);

          callTimeout = setTimeout(() => {
            if (!isCallConnected) {
              call.end();

              document.getElementById('miss_btn').click(); 

              comAlertMsgBox("부재중입니다.<br>상대방이 응답하지 않았습니다.", function () {
                location.href = '/front/Message/Chat/?typ=user&room=<?=$room?>&name=<?=$_SESSION['MEM_IDX']?>&gubun=buy';
              });
            }
          }, 50000); // 50초

          $('.BtnEndCall').off('click').on('click', function () {
            call.end();
            $('.PopApplyOnply2').removeClass('active');
            $('body').removeClass('scroll_lock');
            comAlertMsgBox('통화가 종료되었습니다.', function () {
              location.href = '/front/Message/Chat/?typ=user&room=<?=$room?>&name=<?=$_SESSION['MEM_IDX']?>&gubun=buy';
            });
          });

          let callStartTime = null;
          let callTimerInterval = null;

          call.onEstablished = () => {
            console.log('통화 연결됨 (onEstablished)');
          };

          call.onConnected = () => {
            isCallConnected = true;
            clearTimeout(callTimeout);

            console.log('통화 연결 완료');

            // 통화 시간계산
            $('.call_waiting_ui').hide();
            $('.video_container').show();
            callStartTime = new Date();
            
            let hasInsertedInitialPoint = false;

            callTimerInterval = setInterval(() => {
              const now = new Date();
              const durationSec = Math.floor((now - callStartTime) / 1000);
              const hours = Math.floor(durationSec / 3600);
              const minutes = Math.floor((durationSec % 3600) / 60);
              const seconds = durationSec % 60;

              const hh = String(hours).padStart(2, '0');
              const mm = String(minutes).padStart(2, '0');
              const ss = String(seconds).padStart(2, '0');

              let timeText = '';
              if (hours > 0) {
                timeText = `${hh}:${mm}:${ss}`;
              } else {
                timeText = `${mm}:${ss}`;
              }

              $('.call_duration2').text(timeText);

              // 1. 통화 연결 직후 최초 1회 코인 차감
              if (!hasInsertedInitialPoint) {
                const formData = new FormData();
                formData.append('mem_idx_onply', user_mem_idx);
                formData.append('mem_nickname_onply', user_mem_nickname);
                formData.append('point', price);
                formData.append('send_idx', corp_mem_idx);
                formData.append('point_desc', isVideoCall ? "video" : "call");

                fetch('/front/Message/InsertPoint', {
                  method: 'POST',
                  cache: 'no-cache',
                  body: formData
                })
                .then(res => res.json())
                .then(data => {
                  if (!data || data.rsCode !== '00') {
                    clearInterval(callTimerInterval);
                    call.end();

                    comAlertMsgBox("보유한 코인이 부족하여 통화를 종료합니다.", function () {
                      location.href = '/front/Message/Chat/?typ=user&room=<?=$room?>&name=<?=$_SESSION['MEM_IDX']?>&gubun=buy';
                    });
                  } else {
                    hasInsertedInitialPoint = true; // 차감 성공 시 플래그 ON
                  }
                });
                return; // 첫 차감 이후 아래 코드 실행 방지
              }

              // 2. 이후 time 분이 지났을 때 체크
              if (durationSec > 0 && durationSec % (time * 60) === 0) {
                const checkForm = new FormData();
                checkForm.append('mem_idx', user_mem_idx);

                fetch('/front/Message/CheckPoint', {
                  method: 'POST',
                  cache: 'no-cache',
                  body: checkForm
                })
                .then(res => res.json())
                .then(data => {
                  if (!data || !data.success || data.remain_point < price) {
                    clearInterval(callTimerInterval);
                    call.end();

                    const unitTimeSec = time * 60;
                    const usedUnits = Math.ceil(durationSec / unitTimeSec); // unitTimeSec기준으로 올림처리한다.
                    const totalUsedCoin = usedUnits * price;

                    const endBtn = document.getElementById('end_btn');
                    endBtn.setAttribute('data-duration', timeText);
                    endBtn.setAttribute('data-call-type', isVideoCall ? 'video' : 'call');
                    endBtn.setAttribute('data-price', price);
                    endBtn.setAttribute('data-total-price', totalUsedCoin);
                    endBtn.click();

                    comAlertMsgBox("보유한 코인이 부족하여 통화를 종료합니다.", function () {
                      location.href = '/front/Message/Chat/?typ=user&room=<?=$room?>&name=<?=$_SESSION['MEM_IDX']?>&gubun=buy';
                    });
                  } else {
                    // 포인트 충분 => 다시 차감
                    const deductForm = new FormData();
                    deductForm.append('mem_idx_onply', user_mem_idx);
                    deductForm.append('mem_nickname_onply', user_mem_nickname);
                    deductForm.append('point', price);
                    deductForm.append('send_idx', corp_mem_idx);
                    deductForm.append('point_desc', isVideoCall ? "video" : "call");

                    return fetch('/front/Message/InsertPoint', {
                      method: 'POST',
                      cache: 'no-cache',
                      body: deductForm
                    });
                  }
                })
                .then(res => res.json())
                .then(data => {
                  if (!data || data.rsCode !== '00') {
                    clearInterval(callTimerInterval);
                    call.end();

                    endBtn.click();

                    comAlertMsgBox("보유한 코인이 부족하여 통화를 종료합니다.", function () {
                      location.href = '/front/Message/Chat/?typ=user&room=<?=$room?>&name=<?=$_SESSION['MEM_IDX']?>&gubun=buy';
                    });
                  }
                })
                .catch(error => {
                  console.error('포인트 체크/차감 오류:', error);
                  clearInterval(callTimerInterval);
                  call.end();
                  comAlertMsgBox("오류가 발생하여 통화를 종료합니다.", function () {
                    location.href = '/front/Message/Chat/?typ=user&room=<?=$room?>&name=<?=$_SESSION['MEM_IDX']?>&gubun=buy';
                  });
                });
              }
            }, 1000);
          };

          call.onEnded = () => {
            console.log('통화 종료됨');

            isCallConnected = true;
            clearTimeout(callTimeout);

            if (callTimerInterval) {
              clearInterval(callTimerInterval);
              callTimerInterval = null;
            }

            if (callStartTime) {
              const endTime = new Date();
              const durationSec = Math.floor((endTime - callStartTime) / 1000);
              const hours = Math.floor(durationSec / 3600);
              const minutes = Math.floor((durationSec % 3600) / 60);
              const seconds = durationSec % 60;

              const hh = String(hours).padStart(2, '0');
              const mm = String(minutes).padStart(2, '0');
              const ss = String(seconds).padStart(2, '0');

              let timeText = '';
              if (hours > 0) {
                timeText = `${hh}:${mm}:${ss}`;
              } else {
                timeText = `${mm}:${ss}`;
              }

              const unitTimeSec = time * 60;
              const usedUnits = Math.ceil(durationSec / unitTimeSec); // unitTimeSec기준으로 올림처리한다.
              const totalUsedCoin = usedUnits * price;
              const endBtn = document.getElementById('end_btn');
              endBtn.setAttribute('data-duration', timeText);
              endBtn.setAttribute('data-call-type', isVideoCall ? '영상 온플' : '음성 온플');
              endBtn.setAttribute('data-price', price);
              endBtn.setAttribute('data-total-price', totalUsedCoin);
              endBtn.click();

              comAlertMsgBox(`통화 종료<br>통화 시간: ${timeText}`, function () {
                location.href = '/front/Message/Chat/?typ=user&room=<?=$room?>&name=<?=$_SESSION['MEM_IDX']?>&gubun=buy';
              });
            } else {
              $('.call_waiting_ui').hide();
              comAlertMsgBox('통화가 연결되지 않았습니다.', function () {
                location.href = '/front/Message/Chat/?typ=user&room=<?=$room?>&name=<?=$_SESSION['MEM_IDX']?>&gubun=buy';
              });
            }

            $('.PopApplyOnply2').removeClass('active');
            $('body').removeClass('scroll_lock');
            $('.call_duration2').text('00:00'); // 시간 초기화
          };

          call.onError = (err) => {
            clearTimeout(callTimeout);
            console.error('통화 중 에러:', err);
          };
        });
      })
      .catch(error => {
        console.error('미디어 장치 접근 오류:', error);
        alert('카메라/마이크 권한이 필요합니다.\n' + error.message);
      });
  })
  .catch(error => {
    console.error('초기화 에러:', error);
    alert('SendBird 사용자 등록 또는 인증에 실패했습니다.\n' + error.message);
  });

});

(수신측 코드)
(document).ready(function () {
const APP_ID = ‘<?=_sendbird_app_id?>’;
const USER_ID = ‘<?=$_SESSION['MEM_IDX']?>’; // 수신자 ID
const NICKNAME = ‘<?=$_SESSION['MEM_NICKNAME']?>’; // 수신자 닉네임
const PROFILE = ‘<?=getQueryItem("member","and mem_idx = '".$_SESSION['MEM_IDX']."'","file1")?>’;

  SendBirdCall.init(APP_ID);

  // 사용자 등록 및 인증
  fetch('/front/Message/Sendbird', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ user_id: USER_ID, nickname: NICKNAME, profile: PROFILE }),
  })
    .then(res => res.json())
    .then(() => SendBirdCall.authenticate({ userId: USER_ID }))
    .then(() => SendBirdCall.connectWebSocket()) // WebSocket 연결 추가
    .then(() => {
      console.log('수신자 인증 완료');
      
      // 수신 대기 설정
      SendBirdCall.addListener(USER_ID, {
        onRinging: (call) => {
          const callerId = call._caller?.userId; // 발신자 member 인덱스
          const callerNick = call._caller?.nickname; // 발신자 member 닉네임
          const roomId = call.customItems?.room; // 채팅방 인덱스
          const user_mem_idx = call.customItems?.user_mem_idx; // 채팅방 > 사용자 인덱스
          const corp_mem_idx = call.customItems?.corp_mem_idx; // 채팅방 > 방장 인덱스
          const price = call.customItems?.price; // 채팅방 > 음성/영상 코인
          const time = call.customItems?.time; // 채팅방 > 음성/영상 시간

          incomingCall = call;
          call.accepted = false;
          $('.PopIncomingCall').addClass('active');
          $('body').addClass('scroll_lock');

          const localView = document.getElementById('local_video_element_id');
          const remoteView = document.getElementById('remote_video_element_id');

          const isVideoCall = call.isVideoCall;

          if (isVideoCall) {
            $('.person').show();
            $('#local_video_element_id').css({
              visibility: 'visible',
            });
            $('#remote_video_element_id').css({
              visibility: 'visible',
            });
            $('.waiting_text').css({
              color:'#fff',
            })
            $('.onply_info_text').css({
              color:'#fff',
            })                
          } else {
            $('.person').hide();
            $('#local_video_element_id').css({
              visibility: 'hidden',
              height: '1px'
            });
            $('#remote_video_element_id').css({
              visibility: 'hidden',
              height: '1px'
            });
            $('.video_container').css({
              height: '200px',
              position:'relative'
            });
            $('.local_video_box').css({
              visibility:'hiddne'
            })
            $('.remote_video_box').css({
              visibility:'hiddne'
            })
          }

          const callTypeText = isVideoCall ? '영상온플' : '음성온플';
          if (corp_mem_idx != USER_ID) {
            $('.waiting_text').text(`${callerNick}님의 ${callTypeText} 요청`);
            $('.onply_info_text').text(`분당 : ${price} 코인 / 최소시간 : ${time}분`);
          } else {
            $('.waiting_text').text(`${callerNick}님의 ${callTypeText} 요청`);
            $('.onply_info_text').text('');
          }
          let callStartTime = null;
          let callTimerInterval = null; // 전역 변수로 선언

          // 수락 버튼 클릭 시
          $('.BtnAcceptCall').off('click').on('click', function () {
            if (!isVideoCall) {
              navigator.mediaDevices.getUserMedia({audio: true}).then(stream => {
                const acceptParams = {
                  callOption: {
                    localMediaStream: stream,
                    localMediaView: document.getElementById('local_video_element_id'),
                    remoteMediaView: document.getElementById('remote_video_element_id'),
                    audioEnabled: true,
                    videoEnabled: false
                  }
                };
                call.accept(acceptParams);
              }).catch(err => {
                console.error('오디오 권한 요청 실패:', err);
                comAlertMsgBox('마이크 권한이 필요합니다.');
              });
            } else {
              const acceptParams = {
                callOption: {
                  localMediaView: document.getElementById('local_video_element_id'),
                  remoteMediaView: document.getElementById('remote_video_element_id'),
                  audioEnabled: true,
                  videoEnabled: true
                }
              };
              call.accept(acceptParams);
            }
          });

          // 거절 버튼 클릭 시
          $('.BtnRejectCall').off('click').on('click', function () {
            try {
              if (incomingCall) {
                // decline() 메서드가 없는 경우 end()를 사용
                if (typeof incomingCall.decline === 'function') {
                  incomingCall.decline().catch(error => {
                    console.error('decline 실패:', error);
                    comAlertMsgBox('통화 거절 중 오류가 발생했습니다:\n' + error.message);
                  });
                } else if (typeof incomingCall.end === 'function') {
                  incomingCall.end();  // 연결 종료
                } else {
                  console.error('reject 또는 end 메서드가 없습니다.');
                  comAlertMsgBox('통화를 거절할 수 없습니다.');
                }
              }
            } catch (err) {
              console.error('예외 발생:', err);
              comAlertMsgBox('통화 거절 처리 중 예외가 발생했습니다:\n' + err.message);
            }

            $('.call_waiting_ui').hide();
            $('.PopIncomingCall').removeClass('active');
            $('body').removeClass('scroll_lock');
          });

          let timeoutId = null;
          let isAnswered = false;

          // 통화 연결되었을 때
          call.onConnected = () => {
            isAnswered = true;
            clearTimeout(timeoutId);
            console.log('수신 통화 연결됨');
            
            $('.BtnAcceptCall').hide();
            $('.BtnRejectCall').css('left', 'calc(50% - 40px)'); // 중앙 정렬
            $('.call_waiting_ui').hide();
            $('.video_container').show();

            callStartTime = new Date();
            callTimerInterval = setInterval(() => {
              const now = new Date();
              const durationSec = Math.floor((now - callStartTime) / 1000);
              const hours = Math.floor(durationSec / 3600);
              const minutes = Math.floor((durationSec % 3600) / 60);
              const seconds = durationSec % 60;

              const hh = String(hours).padStart(2, '0');
              const mm = String(minutes).padStart(2, '0');
              const ss = String(seconds).padStart(2, '0');

              let timeText = '';
              if (hours > 0) {
                timeText = `${hh}:${mm}:${ss}`;
              } else {
                timeText = `${mm}:${ss}`;
              }
              
              $('.call_duration').text(`${timeText}`);
            }, 1000);
          };

          // 통화 종료 시
          call.onEnded = () => {
            console.log('통화 종료됨');
            clearTimeout(timeoutId);

            if (!isAnswered) {
              comAlertMsgBox('통화가 연결되지 않았습니다.', function () {
                location.reload();
              });
            } else {

              if (callTimerInterval) {
                clearInterval(callTimerInterval);
                callTimerInterval = null;
              }

              if (callStartTime) {
                const endTime = new Date();
                const durationSec = Math.floor((endTime - callStartTime) / 1000);
                const hours = Math.floor(durationSec / 3600);
                const minutes = Math.floor((durationSec % 3600) / 60);
                const seconds = durationSec % 60;

                const hh = String(hours).padStart(2, '0');
                const mm = String(minutes).padStart(2, '0');
                const ss = String(seconds).padStart(2, '0');

                let timeText = '';
                if (hours > 0) {
                  timeText = `${hh}:${mm}:${ss}`;
                } else {
                  timeText = `${mm}:${ss}`;
                }

                $('.PopIncomingCall').removeClass('active');
                $('body').removeClass('scroll_lock');
                $('.call_duration').text('00:00'); // 시간 초기화
                comAlertMsgBox(`통화 종료<br>통화 시간: ${timeText}`, function () {
                  location.reload();
                });
              }
            } 
          };

          timeoutId = setTimeout(() => {
            if (!isAnswered) {
              call.end();
            }
          }, 50000); // 50초

          call.onError = (err) => {
            console.error('수신자 통화 에러:', err);
          };
        },
      });
    })
    .catch(err => {
      console.error('수신자 초기화 실패:', err);
    });
});

[Frequency]
// How frequently is this issue occurring?
안드로이드에서 아이폰으로 음성통화 연결 시, 안드로이드에서 말하는게 아이폰에서는 안들림.
[Current impact]
// How is this currently impacting your implementation?
Android에서 iPhone으로 통화 연결 시, Android에서 말하는게 아이폰에서는 안들림.
SDK는 현재 프로젝트내에 JS파일로 넣은 상태고 구현은 다 한상태이기 떄문에 버전바꾸기는 어려움.