2.Screen Sharing - Sharing

배고픈 징징이 ㅣ 2024. 8. 9. 13:51

1. 설명

전 글에서 기본적인 Socket 통신이 완료되었다면, 이제 화면을 공유해보자.

혹시 Socket 통신을 아직 구성하지 않았다면 아래 글을 참조하면 된다.

 

 

1. Screen Sharing - SocketIO Setting

1. 설명화면 공유 기술이 필요했다.처음에는 WebRTC를 사용하려 했지만, UDP를 사용하는 WebRTC의 특성상 TURN Server가 필요했다.이 프로젝트에서는 TURN Server를 제공할 여유가 없었다.그래서 TURN Server가

pinokio5600.tistory.com

 

먼저 개발을 진행하면서 있던던 이슈들을 먼저 다루어 보겠다.

이슈 1. 화면을 공유하는 방식

화면을 공유하는 방법은 두가지 정도로 크게 분류가 되었다.

  1. 스크린샷을 찍어 계속 보낸다.
  2. 비디오를 찍어 보낸다.

필자는 여기서 비디오를 보내는 방식을 선택했다.

이미지를 보내는 코드들과 영상을 보내는 코드가 다 있었는데... 테스트를 끝마치고 다 지워버려서...

다른 코드들은 다른 분들의 글을 참조하면 많이있으니 참조하면 될 것 같다.

간략하게 설명하자면,

먼저 공유자가 공유를 시도하면 공유자의 화면을 navigator.mediaDevices.getDisplayMeia로 가져온다.

그리고 그 화면을 MediaRecorder 녹화하여 녹화 중인 영상의 부분을 시청자에게 보낸다.

시청자는 받은 영상 데이터를 MdiaSource로 Buffer를 통해 계속 추가하며 끊기지 않고 본다.

서버는 단순히 받은 데이터를 그대로 시청자들에게 전달만 해준다.

 

2. Screen Sharing

먼저 제일 간단한 서버쪽 소스이다.

공유자의 데이터를 그대로 시청자들에게 전달만 해주면 된다.

import { MessageBody, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer } from "@nestjs/websockets"
import { Server } from "socket.io"

@WebSocketGateway()
export class SharingGateway implements OnGatewayConnection, OnGatewayInit, OnGatewayDisconnect{
    @WebSocketServer() server : Server;
    
    ...

    //클라이언트로 부터 screenData라는 데이터를 받는다.
    @SubscribeMessage("screenData")
    handleScreenSharing(client : Socket, payload : any){        
        //공유자를 제외한 시청자들에게 screenData 전송
        client.broadcast.emit("screenData", payload);
    }
}

 

그 다음으로 이제 공유자가 어떻게 공유를 하는지 소스를 보자.

getDisplayMedia라는 메소드로 공유자의 화면을 가져오게한다.

그러면 이제 가져온 화면을 MediaRecorder로 녹화를 시작한다.

start메소드로 몇초마다 녹화한 데이터를 끊을 것인지 정하고

ondataavailable 메소드로 끊은 데이터를 시청자들에게 보낼것이다.

import { io } from "socket.io-client";

export var ToolSharing = {
    sharing : (function(){
        const socket = io("http://localhost:3080", {transports : ["websocket"]});
        
        ...

        var tool = {
            click : function click(){
                //공유자의 화면을 stream으로 가져온다.
                navigator.mediaDevices.getDisplayMedia({video : true}).then(stream => {
                    //가져온 stream을 녹화한다.
                    const mediaRecorder = new MediaRecorder(stream, {mimeType: 'video/webm; codecs=vp9'});

                    //영상이 준비되면 screenData라는 데이터를 서버로 보낸다.
                    mediaRecorder.ondataavailable = e => {    
                        if (e.data && e.data.size > 0){
                            socket.emit("screenData", e.data);
                        }
                    };
                    
                    //0.05초 마다 녹화 중인 영상을 ondataavailable로 보내준다.
                    mediaRecorder.start(50);
                });
            }
        }

        return tool;
    })()
}

 

그러면 이제 공유자의 영상 전달은 다 끝났다.

문제는 시청자들이 어떻게 이 데이터를 볼 수 있게 해줄것인지에 대한 것인데,

먼저 맨처음에 필자는 영상을 끊어 보냈으니, 영상을 그대로 보여만 주면 될 것이라고 생각했다.

여기서 첫 이슈가 발생했는데, 첫 영상만 정상적으로 재생이 되고 두번째 영상부터는 재생이 안됐다.

여기저기 찾아본 결과, ondataavailable로 넘어온 데이터들 중 첫 데이터에만 meta-data가 들어있어서

첫 영상만 재생이되고 두번째부터는 재생이 안된다는것 같았다.

이에 해결방법을 찾다가 녹화 자체를 끊고 다시 녹화해서 보내면 된다는 설명을 보고 개발을 진행했는데.

이 경우에는 영상이 끊어지고 다시 재생되는 과정에서 화면이 너무 깜박거려서 이미지 전송보다 안좋아 보였다.

계속 해결책을 찾아본 결과, MediaSource라는 것을 찾게 되었다.

MediaSource는 간단하게 끊어진 영상 정보들을 이어주는 우리가 아는 스트리밍을 도와줄 수 있는 객체였다.

몇번의 시도 결과, 부드러운 영상 제공이 가능해져서 이 코드를 공유하도록 한다.

import { io } from "socket.io-client";

export var ToolSharing = {
    sharing : (function(){
        const socket = io("http://localhost:3080", {transports : ["websocket"]});
        
        socket.on('connect', () => {
            ...
            
            let sourceBuffer;
            let latestData = null;
            let mediaSource = new MediaSource();

            //mediaSource가 src랑 연결될때 실행된다.
            mediaSource.addEventListener('sourceopen', () => {                
                sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs=vp9');
                sourceBuffer.mode = 'sequence';
                
                //아직 업데이트 중이여서 지연된다면.
                //영상을 실시간에 최대한 맞추기 위해 가장 최근 영상만 보여주고 중간 영상은 무시한다.
                sourceBuffer.addEventListener('updateend', () => {
                    if (latestData !== null && !sourceBuffer.updating) {
                        try {
                            sourceBuffer.appendBuffer(latestData);
                            latestData = null;
                        } catch (e) {
                            console.error("Failed to append buffer:", e);
                            // Handle error if necessary
                        }
                    }
                });
            });

            const videoElement = document.getElementById("screenVideo");
            videoElement.src = URL.createObjectURL(mediaSource);

            socket.on("screenData", data => {
                const uint8Array = new Uint8Array(data);
                if(!sourceBuffer.updating) sourceBuffer.appendBuffer(uint8Array);
                //sourceBuffer가 업데이트 중이라면 업데이트 완료 후에 추가될 수 있게 한다.
                else {
                    console.log("sourceBuffer.updating O");
                    latestData = uint8Array;
                }

                document.getElementById("viewDiv").style.display = "none";
                //시청자 쪽에서 muted를 안해준다면 에러 발생
                //브라우저가 사용자 자동 재생을 막는 이슈
                videoElement.muted = true;
                videoElement.play();
            });
        });
        
        ...
    })()
}

 

 

여기까지 잘 따라왔다면 영상이 부드럽게 공유되는 것을 볼 수 있을 것이다.

다만 아직 공유자와 시청자간의 시간 차이, 시청자의 이벤트 수신, 공유자와 시청자의 해상도 차이에 따른 문제점 등등 아직 진행해야할 부분이 많다.

다음 글에서는 시청자의 이벤트를 수신하는 부분을 포스팅 해보겠다.

반응형

'NodeJS' 카테고리의 다른 글

4. Screen Sharing - Advancement  (0) 2024.08.14
3.Screen Sharing - Mouse Event  (0) 2024.08.12
1. Screen Sharing - SocketIO Setting  (0) 2024.08.09
NextJs  (0) 2024.07.23
Express & NestJS  (0) 2024.07.16