Signaling - 3편 Spring and Kurento (6)

박근수·2024년 2월 20일
0

이전 포스팅이 Broswer에서 Peer Connection을 관리하는 방법이라면 이번 포스팅에서는 서버단에서 어떻게 Peer Connection을 연결하고 유지하는지 말해보겠습니다. 1:N 스트리밍을 KMS을 이용해서 구현하는 위주로 진행합니다.

Kurento 1:N Streaming example
Kurento Configure
Kurento Install

1:N 스트리밍 설명

사람이 많이 출입하는 곳은 출구와 입구가 다릅니다. 문이 크든 작든 나가려는 사람과 들어오는 사람이 만나면 부딪치고 병목현상이 일어납니다. 네트워크도 이와 유사합니다. A->B로 가는 흐름이 있다면 흐름이 끝나고 나서 A<-B의 흐름을 보내거나 문을 2개 만들고 A->B와 A<-B의 흐름을 따로 관리합니다.
KMS도 이 상태를 벗어날 수 없고 InBound와 OutBound를 처리하는 문이 다릅니다.이 문을 EndPoint라고 표현할 수 있을 것 같습니다.

KMS는 데이터를 보내는 EndPoint를 Src, 들어오는 EndPoint를 Sink라고 표현합니다. 문은 하나라도 통로는 여러개가 될 수 있죠. KMS는 한 Src에서 보내는 데이터를 여러 Sink로 보내야합니다. 1:N 스트리밍을 구현하는 것은 이러한 EndPoint와 통로(Pipeline)을 관리하고 개발자의 의도에 맞게 할당하는 것입니다.

아직 시그널링 서버에서 PeerConnection을 연결하지도 않았음에도 이것을 설명드리는 이유는 이러한 패턴이 낯설어서 Kurento example code를 읽을 때 곤혹스러웠기 때문입니다. 물론 아직도 익숙하지는 않지만 조금이나마 독자분께 도움이 되셨으면 좋겠습니다.

  • 제 코드를 보면서 설명할까 했는데 프로젝트 구현 내용이 섞여 있어서 Peer Connection을 이해하기에는 오히려 복잡할 것 같아서 Kurento Example OneToMany Code를 보겠습니다.


중요한건 KurentoClient입니다. 이것을 통해서 Signaling Server와 KMS가 연결됩니다. 파라미터를 주지 않는다면 같은 서버에 있는 것을 디폴트로 실행됩니다. KMS와 Signaling이 다른 서버에 있다면

    @Bean
    public KurentoClient kurentoClient() {

        String kurentoValue;
        return KurentoClient.create("ws://someURL");
    }

이렇게 설정할 수 있습니다.

private synchronized void presenter(final WebSocketSession session, JsonObject jsonMessage)
      throws IOException {
    if (presenterUserSession == null) {
      presenterUserSession = new UserSession(session);

      pipeline = kurento.createMediaPipeline();
      presenterUserSession.setWebRtcEndpoint(new WebRtcEndpoint.Builder(pipeline).build());

      WebRtcEndpoint presenterWebRtc = presenterUserSession.getWebRtcEndpoint();

      presenterWebRtc.addIceCandidateFoundListener(new EventListener<IceCandidateFoundEvent>() {

        @Override
        public void onEvent(IceCandidateFoundEvent event) {
          JsonObject response = new JsonObject();
          response.addProperty("id", "iceCandidate");
          response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
          try {
            synchronized (session) {
              session.sendMessage(new TextMessage(response.toString()));
            }
          } catch (IOException e) {
            log.debug(e.getMessage());
          }
        }
      });

      String sdpOffer = jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString();
      String sdpAnswer = presenterWebRtc.processOffer(sdpOffer);

      JsonObject response = new JsonObject();
      response.addProperty("id", "presenterResponse");
      response.addProperty("response", "accepted");
      response.addProperty("sdpAnswer", sdpAnswer);

      synchronized (session) {
        presenterUserSession.sendMessage(response);
      }
      presenterWebRtc.gatherCandidates();

    } else {
      JsonObject response = new JsonObject();
      response.addProperty("id", "presenterResponse");
      response.addProperty("response", "rejected");
      response.addProperty("message",
          "Another user is currently acting as sender. Try again later ...");
      session.sendMessage(new TextMessage(response.toString()));
    }
  }

전체 코드는 Kurento OneToMany example에 있습니다.

  private synchronized void presenter(final WebSocketSession session, JsonObject jsonMessage)
      throws IOException {
    if (presenterUserSession == null) {
      presenterUserSession = new UserSession(session);

      pipeline = kurento.createMediaPipeline();
      presenterUserSession.setWebRtcEndpoint(new WebRtcEndpoint.Builder(pipeline).build());

      WebRtcEndpoint presenterWebRtc = presenterUserSession.getWebRtcEndpoint();

KMS를 사용하기 위해서는 EndPoint와 Pipeline이 필수적입니다. 데이터를 받고 보내는 문과 통로에 해당하기 때문입니다. 처음에 시작하면 KMS의 Pipeline을 만들고 그것을 활용하는 EndPoint를 생성합니다. 아직 Sink일지 Src일지 그 성격을 정해지지 않았습니다. 그저 문과 통로일뿐입니다.

String sdpOffer = jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString();
String sdpAnswer = presenterWebRtc.processOffer(sdpOffer);

JsonObject response = new JsonObject();
response.addProperty("id", "presenterResponse");
response.addProperty("response", "accepted");
response.addProperty("sdpAnswer", sdpAnswer);

presenterUserSession.sendMessage(response);

presenterWebRtc.gatherCandidates();

상대 Peer가 보낸 SDPOffer를 받고 WebRtcEndPoint에 등록하면 SDP를 기반으로 EndPoint의 성격이 설정되고 이에 맞는 SDPAnswer가 생성됩니다. 이것을 상대 Peer에게 보냅니다.
SDP교환 이후에는 Ice Candidate가 있어야겠죠
KMS와 상대 Peer가 통신하기 위한 Ice Candidate를 수집합니다.


들여쓰기가 잘 안되서 캡쳐했습니다. ㅠㅠ
여기도 EventListner로 작동하는데 Ice Candidate가 수집되면 상대 Peer에게 전송합니다.

  • 다시 말씀드리지만 ICE Candidate를 수집하는 것까지 구현하는 내용이고 ICE 연결은 개발자가 구현하지 않아도 알아서 작동합니다.

ICE Candidate는 쌍방이 체크하는 것입니다. 따라서 KMS에게도 Coturn 값을 줘야겠죠.
제 프로젝트에서는 Broswer와 KMS의 Coturn이 같아서 굳이 써줘야 하나 싶기도 했는데 KMS 입장에서는 Stun/Turn 설정을 주지 않으면 ICE Candidate를 수집하지 못해서 ICE교환 과정이 원활하지 못한 것 같습니다. 일단 설정해줍니다. Kurento 공식문서 보시면 설정위치가 있습니다.

Kurento는 도커로 실행했습니다. 도커 배쉬에 들어가줍니다.

sudo docker exec -it e758f0f87f42 /bin/bash
vim /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini

ini 파일에 들어가서

stunServerAddress=stun.l.google.com
stunServerPort=19302
turnURL=name:credential@URL:port

stun/turn 설정 값을 주면 KMS가 상대 Peer와 알아서 ICE Candidate를 수집합니다. turnURL은 Coturn에서 설정한 값을 주면 됩니다.

Coturn도 설치하고 설정 값을 주셔야합니다. 관련 블로그가 많으니 참고하시면 될 것 같습니다만 유의할 점이 있습니다. realm이 무엇인지 이해하지 못해서 값을 주지 않았는데 401 unauthorize Error가 계속 발생했습니다.
realm을 설정해주셔야합니다. 추가적으로 Coturn 모니터링 팁을 드릴 수는 있을 것 같습니다.

sudo service coturn status

로 coturn에 들어오는 값들과 coturn이 어떻게 반응했는지 확인할 수 있습니다.

coturn.service - coTURN STUN/TURN Server
     Loaded: loaded (/lib/systemd/system/coturn.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2024-02-05 03:05:45 KST; 5 days ago
       Docs: man:coturn(1)
             man:turnadmin(1)
             man:turnserver(1)
   Main PID: 292618 (turnserver)
      Tasks: 9 (limit: 19165)
     Memory: 6.8M
     CGroup: /system.slice/coturn.service
             └─292618 /usr/bin/turnserver --daemon -c /etc/turnserver.conf --pidfile /run/turnserver/turnserver.pid

Feb 10 09:01:41 ip-xxx-xx-xx-xx turnserver[292618]: 453357: session 003000000000000210: closed (2nd stage), user <> realm <cat>
Feb 10 09:11:24 ip-xxx-xx-xx-xx turnserver[292618]: 453940: IPv4. tcp or tls connected to: 162.216.149.101:61724
Feb 10 09:11:34 ip-xxx-xx-xx-xx turnserver[292618]: 453950: IPv4. tcp or tls connected to: 162.216.149.101:59722
Feb 10 09:11:45 ip-xxx-xx-xx-xx turnserver[292618]: 453961: session 000000000000000206: TCP socket closed remotely 162.216.149>
Feb 10 09:11:45 ip-xxx-xx-xx-xx turnserver[292618]: 453961: session 000000000000000206: usage: realm=<catchup>, username=<>, r>
Feb 10 09:11:45 ip-xxx-xx-xx-xx turnserver[292618]: 453961: session 000000000000000206: closed (2nd stage), user <> realm <cat>
Feb 10 11:38:57 ip-xxx-xx-xx-xx turnserver[292618]: 462793: handle_udp_packet: New UDP endpoint: local addr 172.26.11.74:3478,>
Feb 10 11:38:57 ip-xxx-xx-xx-xx turnserver[292618]: 462793: session 003000000000000211: realm <catchup> user <>: incoming pack>
Feb 10 11:39:57 ip-xxx-xx-xx-xx turnserver[292618]: 462853: session 003000000000000211: usage: realm=<catchup>, username=<>, r>
Feb 10 11:39:57 ip-xxx-xx-xx-xx turnserver[292618]: 462853: session 003000000000000211: closed (2nd stage), user <> realm <cat

싸피 보안 정책으로 IP 지웠습니다!

Trickle ICE로 Stun/Turn을 테스트해봤을 때 내가 생각하는 반응이 나오지 않는다면 위의 명령어로 모니터링 해볼 수 있습니다.


싸피 보안 정책으로 IP와 URL 지웠습니다!

하단에 701 Error로 당황하시더라도 그건 무시하셔도 괜찮습니다. 위에 srflx,relay가 잘 나온다면 괜찮습니다. srflx는 stun,relay는 turn 서버의 응답 타입이기 때문입니다. 결과 값이 안나오고 Error가 발생한다면 그건 문제가 맞습니다. wireshark로 보내는 패킷, 응답 패킷 확인하거나 coturn status나 다른 서버들의 log를 확인해야 합니다.

sion 003000000000000212: realm <catchup> user <>: incoming packet BINDING processed, success
sion 003000000000000212: realm <catchup> user <>: incoming packet message processed, error 401: Unauthorized
4. Local relay addr: 172.26.11.74:52825
sion 003000000000000212: new, realm=<catchup>, username=<username1>, lifetime=600
sion 003000000000000212: realm <catchup> user <username1>: incoming packet ALLOCATE processed, success
sion 003000000000000212: refreshed, realm=<catchup>, username=<username1>, lifetime=0
sion 003000000000000212: realm <catchup> user <username1>: incoming packet REFRESH processed, success
sion 003000000000000212: usage: realm=<catchup>, username=<username1>, rp=4, rb=248, sp=4, sb=416

앞부분을 짤라서 복사하긴 했는데 Trickle ICE와 coturn status를 봤을 때 turn 서버에 잘 접속하고 사용하고 있습니다.
username이나 password 값을 안주면 error 401 Unauthroized가 발생합니다.
또는 Coturn 설정의 문제일 수도 있습니다.
WireShark를 사용하면 Stun,Turn 응답값을 패킷마다 확인할 수도 있습니다. 저는 realm 때문에 Coturn이 말을 안들어서 너무 답답해 WireShark로 모니터링까지 해보긴 했습니다만 그럴 필요까지는 없을 것 같습니다.

소회

어중간하게 WebRTC 프로토콜과 Kurento를 이해해서 더 많은 내용을 읽지 않아서 고생한 것 같습니다. 디버깅하는 방법을 몰라서 이것저것 시도하며 날린 시간이 참 아쉽습니다. 문제가 있을 때 공식 문서를 더 찾아보거나 많은 내용을 애초에 더 읽어보았다면 이해하기 위해서 다른 블로그를 더 찾아보았던 시간을 단축할 수 있었을 것 같습니다.

profile
개성이 확실한편

0개의 댓글