본문 바로가기
개발 시행착오 정리

[안드로이드] 코틀린 WebRTC 화상 통화 구현 진행 과정 및 시행착오 정리

by 문톰 2022. 2. 8.

 

 

 

이글을 읽기전 알아야할 개념

https://aal-izz-well.tistory.com/entry/WEBRTC-%EA%B3%B5%EB%B6%80%ED%95%9C-%EA%B2%83-%EC%A0%95%EB%A6%AC

 

WEBRTC 공부한 것 정리

WebRTC란?  -웹 어플리케이션 및 사이트들이 별도의 소프트웨어나 플러그인 중간자(서버) 없이 음성, 영상 미디어 혹은 텍스트, 파일 같은 데이터를 브라우저끼리 주고받을 수 있게 만든 기술 -P2P

aal-izz-well.tistory.com

 

 

-처음에는 구글링이나 깃허브에 예제를 찾아다녔다.

 

1.FireBase의 FireStore를 시그널링 서버로 사용해서 구현한 예제

https://ichi.pro/ko/kotlin-ui-webrtc-saempeul-252356845295612

 

Kotlin의 WebRTC 샘플

WebRTC 란 무엇입니까? WebRTC는 동료간에 전송되는 비디오, 음성 및 일반 데이터를 지원하는 플랫폼으로 개발자가 강력한 음성 및 비디오 통신 솔루션을 구축 할 수 있도록합니다. 시그널링 서버

ichi.pro

-webrtc로 화상통화를 하기 위해서는 연결하려는 사람 A와B 각각의 sdp와 ice후보를 서로 교환해야 하는데 각자의 ip를 처음에는 서로 모르기 때문에 최초연결시 시그널링 서버를 통해서 교환해야한다

-위의 예제에서는 Firebase의 FireStore(DB)를 시그널링 서버로 대체해서 구현한 예제였다.

이 그림에서 Signaling이라고 적혀있는 부분을 해당 예제는 firebase를 사용해서 대체했다.

 

1)내가 예상하는 예제가 작동하지 않은 원인(확실X)

 

1-1)Ice후보 FireStore에 저장 관련 문제

SdpMid가 offerCandidate는 "video"로 되어있다.

 

SdpMid가 AnswerCandidate에서는 "audio"로 되어있다.

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

 

[Android] Web RTC 샘플 따라하며 공부하기

며칠간 놀기만하니 너무 나태해지는것 같아 새로운 기술을 배워보려한다. Web RTC, 생소한 단어인데 Web Real-Time Communications 즉, 웹을 통한 실시간 통신API를 뜻한다. 추가 프로그램 없이 음성 및 영

parkkas.tistory.com

그렇지만 문제가 생겨버렸다.......

 

1)시그널링 서버 문제

-구글에서 제공해주는 시그널링 서버가 서비스를 중단해버렸다......

관련링크: https://groups.google.com/g/discuss-webrtc/c/H7XuZfgkGH0?pli=1 

 

PSA: AppRTC (https://appr.tc/) and TestRTC (https://test.webrtc.org/) services turned down as of 2021-12-13

PSA: AppRTC (https://appr.tc/) and TestRTC (https://test.webrtc.org/) services turned down as of 2021-12-13 조회수 1222회

groups.google.com

 

2)시그널링 서버 만들기

-그래서 할 수 없이 시그널링 서버를 직접 만들기로 했다.

-Sdp와 ICE후보 교환과정에 대한 이해만 있다면 시그널링 서버는 단지 그것들을 교환할 수 있게 해주면 되기 때문에

할 수 있다고 생각했다.

-감사하게도 어떤분이  Node.js의 socket.io를 사용해서 Web환경에서 구현한 예제가 있었다.

https://surprisecomputer.tistory.com/9

 

3. WebRTC 구현하기(1:1 P2P)

WebRTC 이론부터 구현까지 1. WebRTC 정리하기 2. WebRTC 구현 방식 3. WebRTC 구현하기(1:1 P2P) 4. WebRTC 구현하기(1:N P2P) 5. WebRTC 구현하기(1:N SFU) 6. WebRTC 성능 비교(P2P vs SFU) 1. 서론 이전까지의..

surprisecomputer.tistory.com

 

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

 

GitHub - moonhyung23/Ex_WebRTC_1_kotlin: [안드로이드/코틀린] WebRTC 구현 예제입니다.

[안드로이드/코틀린] WebRTC 구현 예제입니다. Contribute to moonhyung23/Ex_WebRTC_1_kotlin development by creating an account on GitHub.

github.com

 

 

 

댓글