[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파일로 넣은 상태고 구현은 다 한상태이기 떄문에 버전바꾸기는 어려움.