WebRTC 화상채팅(local)

코딩을 합시다·2023년 2월 15일
0

webRtc 화상채팅을 도전하게 되었다.

socket config 파일에서 handler를 지정해주었고 setAllowedOrigns("*")로 요청 url을 전부 허용해 주었다.

MainController 부분은 크게 볼게 없어서 그냥 넘어가겠다.


SignalHandler.java

package com.example.webrtckw.handler;

import java.util.*;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.stereotype.Component;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
// TextWebSocketHandler인터페이스를 구현한다.
public class SignalHandler extends TextWebSocketHandler {
    List<HashMap<String, Object>> sessions = new ArrayList<>(); // 세션 저장 + 세션에 메세지 전달 하기 위한 용도
    static int roomIndex = -1;

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //메시지 발송 // 구독 개념이 아닌 세션들을 찾아서 찾은 세션들한테만 보내주는 개념인듯함
        System.out.println("message : " + message);
        String msg = message.getPayload();
        System.out.println("msg : " + msg);
        JSONObject obj = jsonToObjectParser(msg);

        String roomNumber = (String) obj.get("roomNumber");
        String msgType = (String) obj.get("type");
        HashMap<String, Object> sessionMap = new HashMap<String, Object>();

        if (sessions.size() > 0) {
            for (int i=0; i<sessions.size(); i++) {
                String tempRoomNumber = (String) sessions.get(i).get("roomNumber");
                if (roomNumber.equals(tempRoomNumber)) {
                    sessionMap = sessions.get(i);
                    roomIndex = i;
                    break;
                }
            }

            if(!msgType.equals("file")) {
                // 해당 방에 있는 세션들에만 메세지 전송
                for (String sessionMapKey: sessionMap.keySet()) {
                    if(sessionMapKey.equals("roomNumber")) { // 다만 방번호일 경우에는 건너뛴다.
                        continue;
                    }

                    WebSocketSession webSocketSession = (WebSocketSession) sessionMap.get(sessionMapKey); // webSocketSession == StandardWebSocketSession[id=9b6f63f7-c1b1-9a09-5110-1fdff5973f86, uri=ws://localhost:8080/chating/abc]
                    webSocketSession.sendMessage(new TextMessage(obj.toJSONString()));
                }
            }
        }
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        //소켓 연결
        boolean sessionExist = false;

        String sessionUrl = session.getUri().toString();
        String roomNumber = sessionUrl.split("/signal/")[1];
        int roomIndex = -1;
        if (sessions.size() > 0) {
            for (int i=0; i<sessions.size(); i++) {
                String tempRoomNumber = (String) sessions.get(i).get("roomNumber");
                if (roomNumber.equals(tempRoomNumber)) {
                    sessionExist = true;
                    roomIndex = i;
                    break;
                }
            }
        }

        if (sessionExist) {
            HashMap<String, Object> sessionMap = sessions.get(roomIndex);
            sessionMap.put(session.getId(), session);
        } else {
            HashMap<String, Object> sessionMap = new HashMap<String, Object>();
            sessionMap.put("roomNumber", roomNumber);
            sessionMap.put(session.getId(), session);
            sessions.add(sessionMap);
        }

        super.afterConnectionEstablished(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        //소켓 종료
        if(sessions.size() > 0) { //소켓이 종료되면 해당 세션값들을 찾아서 지운다.
            for (HashMap<String, Object> stringObjectHashMap : sessions) {
                stringObjectHashMap.remove(session.getId());
            }
        }
        super.afterConnectionClosed(session, status);
    }

    private static JSONObject jsonToObjectParser(String jsonStr) {
        JSONParser parser = new JSONParser();
        JSONObject obj = null;
        try {
            obj = (JSONObject) parser.parse(jsonStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

여기는 코드가 너무 길어서 사진으로 남길 수 없어서 어쩔 수 없이 코드로 적었다. 개인적으로 사진이 보기 깔끔한데 ㅠㅠ

세션에 처음 연결이 되면 afterConnectionEstablished이 코드가 실행이 된다.
String roomNumber = sessionUrl.split("/signal/")[1]; 이 코드를 통해 roomNumber을 꺼내서 sessions에서 roomNumber를 통해 각각 어느 채팅방에 접속해 있는지 구분 할 수 있게 해주었다.


chatrooms.html

<!doctype html>
<html lang="en">
<head>
  <title>Websocket Chat</title>
  <meta charset="utf-8">
</head>
<body>
<div>
  <div>
    <div>
      <h3>채팅방 리스트</h3>
    </div>
  </div>
  <div>
    <div>
      <label>방제목</label>
    </div>
    <input type="text" id="roomTitle">
  </div>
  <ul id="roomList">
    <li class="list-group-item list-group-item-action" onclick="enterRoom(1)">
      1, abc
    </li>
    <li class="list-group-item list-group-item-action" onclick="enterRoom(2)">
      2, abc
    </li>
    <li class="list-group-item list-group-item-action" onclick="enterRoom(3)">
      3, abc
    </li>
  </ul>
</div>
<!-- JavaScript -->
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script>
  function makeHtml(roomId, roomTitle) {
    return `<li class="list-group-item list-group-item-action"token interpolation">${roomId}')">
                ${roomId}, ${roomTitle}
            </li>`
  }

  function enterRoom(roomId) {
    localStorage.setItem('roomId', roomId);
    location.href = "/chat/room/" + roomId;
  }

  function createRoom() {
    let roomTitle = $('#roomTitle').val();
    $.ajax({
      type: "POST",
      url: "/chatroom",
      data: {
        roomTitle : roomTitle
      },
      success: function (response) {
        console.log(response);
        let roomId = response.roomId;
        let roomTitle = response.roomTitle;
        let roomOwner = response.roomOwner;
        let roomUUID = response.roomUUID;
        let tempHtml = makeHtml(roomId, roomTitle, roomOwner, roomUUID);
        $('#roomList').append(tempHtml);
        //window.location.reload();
      }
    });
  }
</script>
</body>
</html>

chat.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Chat Room</title>
    <!-- Latest minified Bootstrap & JQuery-->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"></script>
    <!-- Custom styles for this template -->
</head>

<body class="text-center">

<div>
    <div>
        <button id="video_off">Video On</button>
        <button id="video_on">Video Off</button>
        <button id="audio_off">Audio On</button>
        <button id="audio_on">Audio Off</button>

        <div>
            <div>
                <video id="local_video" autoplay playsinline></video>
            </div>
            <div>
                <video id="remote_video" autoplay playsinline></video>
            </div>
        </div>
    </div>
</div>
</body>

<script>
    let roomId = localStorage.getItem('roomId');

    const socket = new WebSocket("ws://" + window.location.host + "/signal/" + roomId); // handler맵핑 url
    const localVideo = document.getElementById('local_video'); // id=local_video 변수
    const remoteVideo = document.getElementById('remote_video'); // id=remote_video 변수

    // UI elements
    const videoButtonOff = document.querySelector('#video_off');
    const videoButtonOn = document.querySelector('#video_on');
    const audioButtonOff = document.querySelector('#audio_off');
    const audioButtonOn = document.querySelector('#audio_on');
    const exitButton = document.querySelector('#exit');

    // WebRTC STUN servers
    const peerConnectionConfig = {
        'iceServers': [
            {'urls': 'stun:stun.stunprotocol.org:3478'},
            {'urls': 'stun:stun.l.google.com:19302'}, // P2P 연결의 중계서버는 구글에서 무료로 지원하는 Google STUN 서버
        ]
    };

    // WebRTC media
    const mediaConstraints = {
        audio: true,
        video: true
    };

    // WebRTC variables
    let localStream;
    let localVideoTracks;
    let myPeerConnection;

    $(function(){
        start(); // window load시 바로 시작
    });

    function start() {
        socket.onmessage = function(msg) {
            let message = JSON.parse(msg.data);
            switch (message.type) {
                case "text":
                    break;
                case "join":
                    handlePeerConnection(message);
                    break;
                default:
                    console.log("start error");
            }
        };

        socket.onopen = function() {
            sendToServer({
                roomNumber: roomId, // uuid를 의미
                type: 'join',
            });
        };

        // a listener for the socket being closed event
        socket.onclose = function(message) {
        };

        // an event listener to handle socket errors
        socket.onerror = function(message) {
        };
    }

    function sendToServer(msg) {
        let msgJSON = JSON.stringify(msg);
        socket.send(msgJSON);
    }

    function handlePeerConnection(message) {
        createPeerConnection(); // peer 생성?
        getMedia(mediaConstraints); // mediaConstraints == autio true video true 변수
    }

    function createPeerConnection() {
        myPeerConnection = new RTCPeerConnection(peerConnectionConfig); // peerConnectionConfig == 구글 peer 서버
    }

    function getMedia(constraints) { // constraints == autio true video true 변수
        if (localStream) {
            alert("이거는 실행됨???@22222222222222222222222");
            localStream.getTracks().forEach(track => {
                track.stop();
            });
        } // navigator.mediaDevices.getUserMedia로 미디어 정보 불러오기??
        navigator.mediaDevices.getUserMedia(constraints) // constratints video audio true 변수 값
            .then(getLocalMediaStream).catch(handleGetUserMediaError);
    }

    /**
     * 카메라/마이크 등 데이터 스트림 접근
     * @param mediaStream
     */
    // add MediaStream to local video element and to the Peer
    function getLocalMediaStream(mediaStream) {
        localStream = mediaStream;
        localVideo.srcObject = mediaStream;
        localStream.getTracks().forEach(track => myPeerConnection.addTrack(track, localStream));
    }

    function handleGetUserMediaError(error) {
        alert("여기가 꼭 실행이 되야하나???")
        switch(error.name) {
            case "NotFoundError":
                alert("Unable to open your call because no camera and/or microphone were found.");
                break;
            case "SecurityError":
            case "PermissionDeniedError":
                // Do nothing; this is the same as the user canceling the call.
                break;
            default:
                alert("Error opening your camera and/or microphone: " + error.message);
                break;
        }
    }

    // mute video buttons handler
    videoButtonOff.onclick = () => {
        localVideoTracks = localStream.getVideoTracks();
        localVideoTracks.forEach(track => localStream.removeTrack(track)); // 비디오 트랙 localStream 제거

        $(videoButtonOff).css('display', 'none');
        $(videoButtonOn).css('display', 'inline');

        $(localVideo).css('display', 'none'); // id=local_video display none;

    };


    videoButtonOn.onclick = () => {
        localVideoTracks.forEach(track => localStream.addTrack(track)); // 비디오 트랙 localStream 추가

        $(videoButtonOff).css('display', 'inline');
        $(videoButtonOn).css('display', 'none');

        $(localVideo).css('display', 'inline'); // id=local_video display none;
    };

    // mute audio buttons handler
    audioButtonOff.onclick = () => {
        $(audioButtonOff).css('display', 'none');
        $(audioButtonOn).css('display', 'inline');

        localVideo.muted = true; // id=local_video mute 활성화;
    };

    audioButtonOn.onclick = () => {
        $(audioButtonOff).css('display', 'inline');
        $(audioButtonOn).css('display', 'none');

        localVideo.muted = false; // id=local_video mute 제거;
    };

    // function video_off_function() {
    //     alert("video_off_function 여기 오긴함???111111111111");
    //     localVideoTracks = localStream.getVideoTracks();
    //     localVideoTracks.forEach(track => localStream.removeTrack(track)); // 비디오 트랙 localStream 제거
    //
    //     $(videoButtonOff).css('display', 'none');
    //     $(videoButtonOn).css('display', 'inline');
    //
    //     $(localVideo).css('display', 'none'); // id=local_video display none;
    // };

    // function video_on_function() {
    //     alert("video on 여기 오긴함22222222");
    //     localVideoTracks.forEach(track => localStream.addTrack(track)); // 비디오 트랙 localStream 추가
    //
    //     $(videoButtonOff).css('display', 'inline');
    //     $(videoButtonOn).css('display', 'none');
    //
    //     $(localVideo).css('display', 'inline'); // id=local_video display none;
    // };

    // function audio_off_function() {
    //     $(audioButtonOff).css('display', 'none');
    //     $(audioButtonOn).css('display', 'inline');
    //
    //     localVideo.muted = true; // id=local_video mute 활성화;
    // };

    // function audio_on_function() {
    //     $(audioButtonOff).css('display', 'inline');
    //     $(audioButtonOn).css('display', 'none');
    //
    //     localVideo.muted = false; // id=local_video mute 제거;
    // };


    // // room exit button handler
    // exitButton.onclick = () => {
    //     stop();
    // };

</script>

</html>

JS 변수.onclick = () ==> {} 을 html button을 통해 구현하였다. 근데 그냥 label로 해도 되긴한다. 적절한 것을 골라서 쓰자

프론트엔드에 관한 설명은 주석으로 대체하겠다 나는 프론트 엔드가 아니기 때문에 자세하게 들어가지는 못할꺼 같다.


application.properties

#H2DB
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.type.descriptor.sql=trace

#Thymeleaf develop
spring.thymeleaf.cache=false

logging.level.org.springframework= info
logging.level.com.example.webrtc=debug

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'

성공 기념 인증샷

아직 갈 길이 멀지만 local에서 내 화면만 띄우는 것은 크게 어려운 점이 없었다.

얼른 WebRtc를 더 공부하고 싶다.

0개의 댓글