이글을 읽기전 알아야할 개념
-처음에는 구글링이나 깃허브에 예제를 찾아다녔다.
1.FireBase의 FireStore를 시그널링 서버로 사용해서 구현한 예제
https://ichi.pro/ko/kotlin-ui-webrtc-saempeul-252356845295612
-webrtc로 화상통화를 하기 위해서는 연결하려는 사람 A와B 각각의 sdp와 ice후보를 서로 교환해야 하는데 각자의 ip를 처음에는 서로 모르기 때문에 최초연결시 시그널링 서버를 통해서 교환해야한다
-위의 예제에서는 Firebase의 FireStore(DB)를 시그널링 서버로 대체해서 구현한 예제였다.
1)내가 예상하는 예제가 작동하지 않은 원인(확실X)
1-1)Ice후보 FireStore에 저장 관련 문제
FireStore(DB)에 ice후보를 저장할 때 위의 그림의 answerCandidate의 sdpMid가 "audio"로 되어있는 부분을 "video"로 저장할 수 있는 방법을 찾아보았지만 찾지 못했다.
1-2)예제 코드상에서의 오류
fun addIceCandidate(iceCandidate: IceCandidate?) {
Log.e("RTClient", "289")
peerConnection?.addIceCandidate(iceCandidate)
}
화상통화를 하려는 상대방의 ice후보를 추가하는 코드이다
-상대방의 ice후보만 추가되어야 하는데 디버깅을 해보니 처음에 나의 ice후보를 생성해서 FireStore에 저장할 때도 이 코드가 작동해서 상대방의 ice후보를 추가하는 것만 아니라 나의 ice후보까지 추가하고 있었던 것이 원인이지 않을까 생각했다.
2)결론
-이후에도 계속해서 깃허브에 다른 예제들을 찾아보거나 구글링을 통해 문제의 원인을 찾아보았지만 찾을 수 없었다.
-고심끝에 FireStore(Db)를 사용해서 WebRTC를 구현하는 예제말고 Google제공하는 WebRTC예제를 보았다.
2.Google에서 제공하는 WebRTC예제
-구글링을 해보니 감사하게도 잘 정리해서 올려 놓아주셨다.
https://parkkas.tistory.com/13
그렇지만 문제가 생겨버렸다.......
1)시그널링 서버 문제
-구글에서 제공해주는 시그널링 서버가 서비스를 중단해버렸다......
관련링크: https://groups.google.com/g/discuss-webrtc/c/H7XuZfgkGH0?pli=1
2)시그널링 서버 만들기
-그래서 할 수 없이 시그널링 서버를 직접 만들기로 했다.
-Sdp와 ICE후보 교환과정에 대한 이해만 있다면 시그널링 서버는 단지 그것들을 교환할 수 있게 해주면 되기 때문에
할 수 있다고 생각했다.
-감사하게도 어떤분이 Node.js의 socket.io를 사용해서 Web환경에서 구현한 예제가 있었다.
https://surprisecomputer.tistory.com/9
2-1)대략적인 흐름
2-2)서버코드
//소켓서버 세팅
const express = require('express');
const server = express();
const http = require('http').Server(server);
//클라이언트와 서버가 소켓 연결을 할 때 사용하는 소켓
const io = require('socket.io')(http);
const port = 5001;
//Room안에 있는 유저 객체
let users = {};
//Room을 담고있는 객체
let socketToRoom = {};
//Room안에 들어갈 수 있는 최대 인원 수
const maximum = 2;
server.get('/', function (req, res) {
res.send('Hello World');
console.log("serverGet");
//소켓 연결 시 클라이언트에 전달해줄 html 문서
// res.sendFile(__dirname + '/client.html');
});
//소켓 연결이 되면 실행되는 메서드
//서버와 클라이언트가 연결되면 소켓을 하나 생성한다.
io.on('connection', socket => {
//연결성공
console.log(`Socket connected(소켓 id) : ${socket.id}`)
//방 입장
socket.on('join_room', data => {
console.log("data.roomName(방제목): " + data.roomName);
//user[room]에는 room에 있는 사용자들이 배열 형태로 저장된다.
//room이 존재하면
if (users[data.roomName]) {
//방에 입장한 사람의 수
const length = users[data.roomName].length;
//최대 인원을 충족시켰으면 더 이상 접속 불가
if (length === maximum) {
console.log("방 정원초과");
//방 인원이 가득찬 경우
socket.to(socket.id).emit('room_full');
return;
} else {
console.log("방입장(배열저장)");
//인원이 최대 인원보다 적으면 접속 가능
//-data.roomName(방제목)으로 내가 속한 방(배열)을 찾는다.
//내가 속한 방에 나의 socket.id를 저장한다.
users[data.roomName].push({id: socket.id})
}
} else {
//room이 존재하지 않는다면 방 새로 생성 (배열 생성)
//data.roomName: 방제목
//socket.id: 유저의 소켓아이디
console.log("방생성");
users[data.roomName] = [{id: socket.id}];
}
// 해당 소켓이 어느 room에 속해있는 지 알기 위해 저장
//socket.id: 유저의 socket.id
//data.roomName: 유저가 속한 방 제목
socketToRoom[socket.id] = data.roomName;
//방 입장
//입력값으로 방제목(data.roomName)을 입력해서 유저를 방에 입장시킨다.
socket.join(data.roomName);
console.log("socketToRoom(방제목), 참여자 Socket.id: " + `[${socketToRoom[socket.id]}]: ${socket.id}`);
// 본인을 제외한 같은 room의 user array
const usersInThisRoom = users[data.roomName].filter(user => user.id !== socket.id);
//나를 제외했기 때문에 인원수가 2명이면 1명이 된다.
console.log("usersInThisRoom(본인제외 방 인원수): " + usersInThisRoom.length);
// 본인에게 해당 방의 user array를 전송
// 새로 접속하는 user가 이미 방에 있는 user들에게 offer(signal)를 보내기 위해
// user array -> 본인을 제외한 본인이 속한 방의 유저 socket.id
io.sockets.to(socket.id).emit('all_users', usersInThisRoom);
});
// 다른 user들에게 offer를 보냄 (자신의 RTCSessionDescription)
socket.on('offer', sdp => {
// room에는 두 명 밖에 없으므로 broadcast 사용해서 전달
socket.broadcast.emit('getOffer', sdp);
console.log('offer: 상대방에게 offer 전달완료');
});
// offer를 보낸 user에게 answer을 보냄 (자신의 RTCSessionDescription)
socket.on('answer', sdp => {
// room에는 두 명 밖에 없으므로 broadcast 사용해서 전달
// 여러 명 있는 처리는 다음 포스트 1:N에서...
socket.broadcast.emit('getAnswer', sdp);
console.log('answer: 상대방에게 answer 전달완료 ');
});
// 자신의 ICECandidate 정보를 signal(offer 또는 answer)을 주고 받은 상대에게 전달
socket.on('candidate', candidate => {
// room에는 두 명 밖에 없으므로 broadcast 사용해서 전달
// 여러 명 있는 처리는 다음 포스트 1:N에서...
socket.broadcast.emit('getCandidate', candidate);
console.log('ICE후보 전달완료');
})
// user가 연결이 끊겼을 때 처리
socket.on('disconnect', () => {
console.log(`[${socketToRoom[socket.id]}]: ${socket.id} exit`);
// disconnect한 user가 포함된 roomID
const roomID = socketToRoom[socket.id];
// room에 포함된 유저
let room = users[roomID];
// room이 존재한다면(user들이 포함된)
if (room) {
// disconnect user를 제외
room = room.filter(user => user.id !== socket.id);
users[roomID] = room;
}
// 어떤 user가 나갔는 지 room의 다른 user들에게 통보
socket.broadcast.to(room).emit('user_exit', {id: socket.id});
console.log(users);
})
});
//5001번 포트로 들어오는 접속 받기.
http.listen(port, function () {
console.log('server on! port: ' + port);
});
3.최종 구현
-왼쪽은 가상디바이스 화면이고 오른쪽은 내 스마트폰 화면입니다.
4.Github
-서버 코드는 위의 있는 Node.js를 사용한 javaScript코드를 참고해주세요.
https://github.com/moonhyung23/Ex_WebRTC_1_kotlin
'개발 시행착오 정리' 카테고리의 다른 글
[안드로이드] 리사이클러뷰 안에 리사이클러뷰 사용시 안에 있는 리사이클러뷰가 터치가 안될 때 (0) | 2023.01.05 |
---|---|
[안드로이드] 코틀린 ViewModel + LiveData 이벤트 발생 이슈 해결 (0) | 2022.04.27 |
운동친구 구하는 어플 기획에서 이상한 부분 (0) | 2022.02.14 |
[안드로이드] Java서버 실시간 단체 채팅 구현 과정 및 시행착오 정리 (0) | 2022.02.10 |
[안드로이드] 이미지 여러 장 처리 시행착오 및 구현 과정 (1) | 2022.01.27 |
댓글