1. 설명
전 글까지 해서 필요한 기능들은 구현이 끝났다.
이전 글을 안봤다면 아래 링크 참조.
1. Screen Sharing - SocketIO Setting
1. 설명화면 공유 기술이 필요했다.처음에는 WebRTC를 사용하려 했지만, UDP를 사용하는 WebRTC의 특성상 TURN Server가 필요했다.이 프로젝트에서는 TURN Server를 제공할 여유가 없었다.그래서 TURN Server가
pinokio5600.tistory.com
2.Screen Sharing - Sharing
1. 설명전 글에서 기본적인 Socket 통신이 완료되었다면, 이제 화면을 공유해보자.혹시 Socket 통신을 아직 구성하지 않았다면 아래 글을 참조하면 된다. 1. Screen Sharing - SocketIO Setting1. 설명화면 공
pinokio5600.tistory.com
3.Screen Sharing - Mouse Event
1. 설명저번 글까지 화면 공유 기능을 완료했다.이번 글에서는 현재 탭을 공유하는 기능과 시청자의 마우스 이벤트를 공유자의 화면에서 발생시킬 수 있도록 개발하겠다.참고로 mouseMove 이벤트
pinokio5600.tistory.com
이 글에서는 이제 구현한 기능들을 고도화 하는 마지막 글이 될 것이다.
하나하나 고도화를 진행해보자.
2. 공유 화면 싱크
만든 프로젝트로 공유 기능을 테스트 해본다면 시청자 화면의 영상 속도가 많이 늦다는걸 볼 수 있다.
또한 시간이 흐르면서 점점 더 과거 시간의 화면이 보일 것이다.
일단 이 문제는 영상 데이터가 늦게 와서 발생하는 문제가 아니라.
시청자 화면에서 영상 싱크를 못맞춰 주는 것이다.
서버 문제로 영상이 과거 시간의 화면이 보일 수 있지만, 이정도로 느린 문제는 서버 문제가 아니라 클라이언트에서 데이터를 정상적으로 보여주지 못하는 문제이다.
아래 코드로 처리가 가능하다.
socket.on('connect', () => {
let sourceBuffer : any;
let latestData : any = null;
let mediaSource = new MediaSource();
...
socket.on("screenData", (data : any) => {
const uint8Array = new Uint8Array(data);
if(!sourceBuffer.updating){
//처음에 sourceBuffer.buffered의 길이가 0 으로 예외 발생
//조건문을 쓰지않고 try문으로 처리
try{
//버퍼에 가장 마지막 부분(가장 공유자의 화면 데이터와 비슷한 시간)과
//시청자가 시청중인 영상의 시간 차이가 0.2초가 넘는다면
//가장 마지막 영상으로 넘긴다.
if(sourceBuffer.buffered.end(0) - videoElement.currentTime > 0.2){
videoElement.currentTime = sourceBuffer.buffered.end(0);
}
}catch(e){
console.log("sourceBuffer.buffered.end(0) error, sourceBuffer.buffered.length ->", sourceBuffer.buffered.length);
}
try {
sourceBuffer.appendBuffer(uint8Array);
} catch (e) {
console.error("Failed to append buffer:", e);
}
} else {
console.log("sourceBuffer.updating O");
latestData = uint8Array;
}
...
});
});
3. 마우스 이벤트의 과도한 전송
특히나 클릭 이벤트의 경우 한번만 보내면 되는데 몇십개의 이벤트를 전송하는 이슈가 있다.
이동 이벤트의 경우에도 너무 많은 이벤트를 전송한다.
이렇게 이벤트를 너무 많이 보내버리면, 트래픽이 너무 많이 발생해 서버에 많은 부하가 와서 느려진다.
이에 throttle이라는 메소드를 생성하여 이벤트 전송을 제한했다.
socket.on('connect', () => {
...
socket.on("screenData", (data : any) => {
...
//이동 이벤트의 경우 0.1초마다 보내도 크게 문제가 없어 보였다.
videoElement.addEventListener("mousemove", throttle(function(event : MouseEvent){
socket.emit("mouseMove", {
x: event.clientX,
y: event.clientY
});
}, 100));
//클릭 이벤트도 0.1초에 한번만 전송하도록 제한하였다.
//각자 상황에 맞게 튜닝을 하면 될 것 같다.
videoElement.addEventListener("click", throttle(function(event : MouseEvent){
socket.emit("mouseClick", {
x: event.clientX,
y: event.clientY
});
}, 100));
});
});
let lastFunc: any;
let lastRan: any;
function throttle(func : any, limit : number) {
return function(this : any, ...args: any) {
if (!lastRan) {
func.apply(this, args);
lastRan = Date.now();
} else {
//func의 이전 setTimeout을 제거한다
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(this, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
4. 버퍼 정리
이미 다 본 영상의 버퍼가 계속 남아있어서 시간이 지날수록 렉이 발생하였다.
이에 일정 시간마다 더 이상 필요없는 버퍼들을 지워주는 로직을 추가하였다.
socket.on('connect', () => {
let sourceBuffer : any;
let latestData : any = null;
//밀리초로 10초마다 버퍼들을 정리한다.
const REMOVE_INTERVAL = 10000;
let lastRemoveTime = 0;
let mediaSource = new MediaSource();
...
socket.on("screenData", (data : any) => {
const uint8Array = new Uint8Array(data);
if(!sourceBuffer.updating){
const currentTime = videoElement.currentTime;
const buffered = sourceBuffer.buffered;
const now = Date.now();
if (now - lastRemoveTime > REMOVE_INTERVAL && buffered.length > 0) {
//밀리초를 초로 변경
const removeEnd = currentTime - (REMOVE_INTERVAL / 1000);
if (buffered.start(0) < removeEnd) {
sourceBuffer.remove(buffered.start(0), removeEnd);
lastRemoveTime = now;
latestData = uint8Array;
}
}
...
//remove 진행중에는 append가 안되는 이슈 발생
//latestData에 넣기에는 어짜피 0.1초마다 최신 버퍼를 전달 받기에 무시하기로 해서 try문을 사용
try {
sourceBuffer.appendBuffer(uint8Array);
} catch (e) {
//console.error("Failed to append buffer:", e);
}
} else {
console.log("sourceBuffer.updating O");
latestData = uint8Array;
}
...
});
});
5. 해상도에 따른 마우스 포인터
해상도에 따라 마우스 포인터의 위치가 잘못잡히고 있다.
이에 공유자의 화면 크기를 기준으로 시청자 화면과 비율 계산을 하여 새로운 포인터 좌표를 구하도록 하였다.
좀 더 정확한 좌표는 더 보완해야 할 것 같다.
먼저 공유자는 화면을 공유하기 전에 자신의 화면 크기를 전달한다.
const funcStartScreenSharing = () => {
//@ts-ignore
navigator.mediaDevices.getDisplayMedia({video : true, preferCurrentTab : true}).then(stream => {
const mediaRecorder = new MediaRecorder(stream, {mimeType: 'video/webm; codecs=vp9'});
//공유자의 화면 크기 전달
socket.emit("screenSize", {
width : window.innerWidth,
height : window.innerHeight
});
...
});
const remoteCursor : HTMLElement | null = document.getElementById("remoteCursor");
if(remoteCursor){
//@ts-ignore
remoteCursor.style="position: absolute; width: 10px; height: 10px; background: red; z-index : 50; pointer-events: none;";
socket.on("mouseMove", (data: { x: string; y: string; }) => {
remoteCursor.style.left = data.x + "px";
remoteCursor.style.top = data.y + "px";
});
}
socket.on("mouseClick", (data: { x: number; y: number; }) => {
const targetElement = document.elementFromPoint(data.x, data.y);
if(targetElement){
const event = new MouseEvent("click", {
clientX : data.x,
clientY : data.y,
bubbles : true,
cancelable : true
});
targetElement.dispatchEvent(event);
}
});
}
시청자는 받은 화면 크기를 기준으로 새로운 좌표를 계산하여 보낸다.
socket.on('connect', () => {
let screenWidth = 0, screenHeight = 0;
socket.on("screenSize", (message : any) => {
screenWidth = message.width;
screenHeight = message.height;
});
...
socket.on("screenData", (data : any) => {
...
const rect = videoElement.getBoundingClientRect();
const scaleX = screenWidth / rect.width;
const scaleY = screenHeight / rect.height;
videoElement.addEventListener("mousemove", throttle(function(event : MouseEvent){
socket.emit("mouseMove", {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY
});
}, 100));
videoElement.addEventListener("click", throttle(function(event : MouseEvent){
socket.emit("mouseClick", {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY
});
}, 100));
});
});
5. 퍼포먼스
속도를 위해 공유자쪽에서 여러가지 테스트해본 결과
FPS를 조정했다.
기존 30프레임에서 24프레임으로 낮췃는데, 24프레임은 영화에서 사용하는 프레임으로 시청에 문제가 없어서 채택했다.
15프레임 이하는 너무 끊겨서 사용하기에 부적합해 보였다.
코덱은 vp9보다 vp8이 더 적합했다.
화질 자체도 vp8이 많이 떨어지지 않고, 속도가 더 빨랐다.
영상 전송 주기는 0.05초로 낮췄다.
0.1초보다 0.05초일때가 더욱 부드러웠다.
영상 전송 주기 변경으로 시청자쪽 버퍼 제거와 싱크 맞추기는 똑같이 반으로 줄여주면 된다.
const funcStartScreenSharing = () => {
const userInfo = getUserInfo();
if(userInfo){
socket.emit("enterRoom", userInfo.userId);
//@ts-ignore
navigator.mediaDevices.getDisplayMedia({video : {frameRate : 24}, preferCurrentTab : true}).then(stream => {
const mediaRecorder = new MediaRecorder(stream, {mimeType: 'video/webm; codecs=vp8'});
...
mediaRecorder.start(50);
...
});
...
}
}
6. 실패
SocketIO로 미디어 데이터를 통신하는 것은 부적합하다고 결론을 짓게됐다.
지연이 WebRTC에 비해 너무 길어서 마우스 포인터를 공유하여 공동 작업을 하려는 목적에 맞지 않았다.
좀 더 다듬어서 서비스할 정도로 만들 수 도 있겠지만, 작업에 필요한 시간이 너무 길어질 것 같아서 비효울적이라 포기하게 되었다.
단순 화면을 공유해주는 정도라면 SocketIO만으로도 가능할 것 같다.
'NodeJS' 카테고리의 다른 글
WebTerminal - Prototype (0) | 2024.10.28 |
---|---|
WebRTC - SFU, Mediasoup (6) | 2024.08.27 |
3.Screen Sharing - Mouse Event (0) | 2024.08.12 |
2.Screen Sharing - Sharing (0) | 2024.08.09 |
1. Screen Sharing - SocketIO Setting (0) | 2024.08.09 |