Zoom Clone Coding part 3
Zoom Clone Coding
Adapter을 이용해서 server에 존재하는 room 개수를 구하고, room에 접속해있는 client 개수를 구할 수 있다. 또한 socket.io 에서는 admin-ui panel을 제공해서 전체적인 server, client을 관리할 수 있다.
javascript의 navigator.MediaDevices을 이용하면 현재 브라우져 연결된 카메라, 오디오 장비의 현황을 추출할 수 있다. 이를 통해 직접적인 장비의 조작이 가능하다.
Adapter
Adapter은 serverside에 동작하면서 server와 client간에 발생하는 event를 관장한다.
//io.sockets.adapter 객체의 일부분이다.
rooms: Map(4) {
'dphIvduRoqcRPamkAAAF' => Set(1) { 'dphIvduRoqcRPamkAAAF' },
'8AjVVPxb8XCZtKogAAAH' => Set(1) { '8AjVVPxb8XCZtKogAAAH' },
'UxuOM5WuRuVonnPNAAAJ' => Set(1) { 'UxuOM5WuRuVonnPNAAAJ' },
'123' => Set(1) { '8AjVVPxb8XCZtKogAAAH' }
},
sids: Map(3) {
'dphIvduRoqcRPamkAAAF' => Set(1) { 'dphIvduRoqcRPamkAAAF' },
'8AjVVPxb8XCZtKogAAAH' => Set(2) { '8AjVVPxb8XCZtKogAAAH', '123' },
'UxuOM5WuRuVonnPNAAAJ' => Set(1) { 'UxuOM5WuRuVonnPNAAAJ' }
}
위와 같이 해당 서버에 연결된 socket의 id를 저장하는 sids나 room을 저장하는 rooms가 있다. 그래서, adapter을 이용하면 서버 내에 연결된 방의 개수나, 방 안에 존재하는 유저 수를 구할 수 있다.
Room Count
const io=SocketIO();
io.socket.adapter.rooms
rooms 객체의 entry 개수를 구하면 된다.
server.js
function publicRooms(){
/*
const sids=io.sockets.adapter.sids;
const rooms=io.sockets.adapter.rooms;
*/
const {
sockets:{
adapter:{
sids,rooms
}
}
}=io;
//public room만을 가져오기 위해 sid에 등록된 id값과 같은 room id는 제외시킨다.
const public_rooms={};
rooms.forEach((_,key)=>{
if(sids.get(key) === undefined){
public_rooms[key]=rooms.get(key)?.size;
}
})
return public_rooms;
}
추가로, socket ID 별로 서버와 client간에 private 연결이 유지되고 있다 따라서, rooms 객체를 보면 sids에 등록된 ID에 해당하는 값들이 rooms에도 포함되어 있음을 확인 할 수 있다. 따라서 이러한 private room은 제거하고 나머지 방들에 대한 정보를 가져와야 한다.
User Count
rooms: Map(4) {
'dphIvduRoqcRPamkAAAF' => Set(1) { 'dphIvduRoqcRPamkAAAF' },
'8AjVVPxb8XCZtKogAAAH' => Set(1) { '8AjVVPxb8XCZtKogAAAH' },
'UxuOM5WuRuVonnPNAAAJ' => Set(1) { 'UxuOM5WuRuVonnPNAAAJ' },
'123' => Set(1) { '8AjVVPxb8XCZtKogAAAH' }
},
또한, rooms 객체를 보면 아래와 같이 key 에 대한 value로 SET가 등록되어 있는데, 이는 해당 room에 접속된 client 집단을 의미한다. 이를 이용하면 room 내부에 user수를 쉽게 구할 수 있다.
server.js
function countRoom(roomName){
return io.sockets.adapter.rooms.get(roomName)?.size;
}
추가적으로 생각해야되는 부분은 user가 접속하고, 나갈때의 변화(event)를 감지해서 room 정보를 변경시켜줘야한다.
server.js
socket.to(roomName).emit("welcome",socket.nickname,countRoom(roomName));
socket.on("disconnecting",()=>{
socket.rooms.forEach(room => socket.to(room).emit("bye",socket.nickname,countRoom(room)-1));
}
);
유저가 들어오고 나가게 되면 해당 방에 접속되어 있는 유저들에게 변경된 방의 정보를 넘겨줘야한다.
socket.emit("room_change",publicRooms())
io.sockets.emit("room_change",publicRooms());
socket.on("disconnect",()=>io.sockets.emit("room_change",publicRooms()));
유저가 처음 접속했을 때는 server내에 있는 room list을 볼 수 있어야 한다.
app.js
socket.on("room_change",(rooms)=> {
const roomList=welcome.querySelector("ul");
roomList.innerHTML="";
if(Object.keys(rooms).length ===0){
return;
}
for(let [roomName,count] of Object.entries(rooms)){
const li=document.createElement("li");
li.innerText=`Room ${roomName} (${count})`;
roomList.appendChild(li);
};
}
또한 room 객체를 전달 받은 front 에서는 위와 같이 room 들을 list에 등록해주는 작업을 한다.
Admin-ui
socket.io 에서는 server에 등록된 socket, client에 대한 관리를 할 수 있는 관리페이지를 제공해준다.
- 이를 사용하기 위해 우선 @socket.io/admin-ui를 node로 설치해준다.
npm install @socket.io/admin-ui
- server에서 필요한 모듈들을 import 해준다.
server.js
import express from "express";
import http from "http";
//admin-ui를 사용하기 위해 import 방식을 바꿔준다.
import {Server} from "socket.io";
import {instrument} from "@socket.io/admin-ui"
const app=express();
app.set("view engine","pug");
app.set("views",__dirname +"/views");
app.use("/public",express.static(__dirname+ "/public"));
app.get("/",(req,res) => res.render("home"));
app.get("/*",(req,res) => res.redirect("/"));
const handleListen = () => console.log(`Listening on http://localhost:3000`);
//app.listen(3000,handleListen);
//http server => to get access to server
const server=http.createServer(app);
//아래와 같이 서버 등록 방식을 새롭게 한다.
const io=new Server(server,{
cors:{
origin:["https://admin.socket.io"],
credentials:true,
}
})
instrument(io,{
auth:false
});
- 그런 다음 http://admin.socket.io에 접속한 후, 서버 url을 입력한 다음 들어가면 관리 페이지에 접속가능하다.
Navigator.MediaDevices
javascript을 이용하면 브라우져에 등록된 카메라, 오디오 장비에 대한 조작이 가능하다.
home.pug
video#myFace(autoplay,playsinline,width="400",height="400")
app.js
async function getMedia(){
try{
myStream= await navigator.mediaDevices.getUserMedia(
{
audio:true,
video:true
}
)
myFace.srcObject=myStream;
}catch(e){
console.log(e)
}
}
getMedia();
위와 같이 navigator.mediaDevices.getUserMedia을 이용하여 현재 브라우져에 등록된 MediaStream 형태로 반환받으며 이를 video tag에 등록하면 현재 카메라 화면이 브라우져 띄워진다.
Camera
그러면 Camera 정보는 어떻게 받아올까?
home.pug
button#camera Turn Camera Off
select#cameras
app.js
async function getCameras(){
try{
const devices=await navigator.mediaDevices.enumerateDevices();
const cameras=devices.filter(device => device.kind==="videoinput");
const currentCamera=myStream.getVideoTracks()[0];
cameras.forEach((camera)=>{
const option=document.createElement("option");
option.value=camera.deviceId;
option.innerText=camera.label;
if(currentCamera.label === camera.label){
option.selected=true;
}
camerasSelect.appendChild(option);
});
console.log(cameras);
}catch(e){
console.log(e);
}
}
이는 enumerateDevices()를 이용한다. 이를 이용하게 되면, 해당 브라우져에 연결된 모든 비디오,오디오 장비의 목록을 확인할 수 있다. 이중에서 카메라는 kind가 “videoinput”이다. 이에 해당하는 모든 카메라를 찾아 select에 등록 시켜준다.
const currentCamera=myStream.getVideoTracks()[0];
추가로, 아래와 같이 현재등록된 MediaStream의 첫번째 video track은 현재 설정된 카메라 값이다.
Camera ON/OFF
현재 등록된 MediaStream의 videotrack의 enabled property을 조작하면 카메라 on/off를 쉽게 할 수 있다.
app.js
function handleCameraClick(){
myStream.getVideoTracks().forEach(track=>track.enabled= !track.enabled)
if(!cameraOff){
cameraButton.innerText="Turn Camera Off";
cameraOff=true;
}
else{
cameraButton.innerText="Turn Camera On";
cameraOff=false;
}
}
cameraButton.addEventListener("click",handleCameraClick);
Change Camera
app.js
async function getMedia(deviceId){
const cameraConstraints={
audio:true,
video:{deviceId:{exact: deviceId}}
};
try{
myStream= await navigator.mediaDevices.getUserMedia(
cameraConstraints
)
myFace.srcObject=mySteam;
}catch(e){
console.log(e)
}
}
function handleCameraSelectChange(){
await getMedia(camerasSelect.value);
}
camerasSelect.addEventListener("input",handleCameraSelectChange);
위와 같이 getUserMedia에 들어가는 constraint 인자를 적절히 설정하면 특정 카메라로 MediaStream을 등록할 수 있다.
Audio
현재 등록된 MediaStream의 audio track의 enabled property을 조작하면 마이크 on/off를 쉽게 할 수 있다.
app.js
function handleMuteClick(){
myStream.getAudioTracks().forEach(track=>track.enabled= !track.enabled)
if(!muted){
muteButton.innerText="Unmute"
muted=true;
}
else{
muteButton.innerText="Mute"
muted=false;
}
}
muteButton.addEventListener("click",handleMuteClick);
Appendix
JavaScript에서는 비동기처리를 위해 Promise, Async&Await 방식을 제공한다.
References
Link: nomadcoders
Link: Adapter
Link: MediaDevice
Link: Async&Await
댓글남기기