'자리 이탈 감지 캠스터디 관리 웹 서비스'로 주제를 정했다.
캠스터디는 혼자서 공부하는 사람들을 대상으로 각자의 공간에서 공부하는 모습을 캠으로 서로 보여주면서 열심히 공부하자는 의지를 함께 불태울 수 있는 서비스이다.
즉, 캠스터디는 서로가 공부를 열심히 하고 있는지 확인할 수 있는 온라인 독서실이다.
캠스터디는 보통 공부시간을 기준으로 캠스터디 방의 규칙을 정하는데,
기존의 캠스터디는 사용자가 직접 수동으로 타이머를 눌러 공부 시간을 기록하여, 목표 공부시간을 채웠는지 인증해야한다.
그런데 이 공부시간을 직접 기록하는 것에 대해 캠스터디를 애용하는 몇몇 사용자의 의견을 물었는데, 다음과 같다.
"공부하다가 가끔 타이머를 멈추지 않아 공부하지 않은 시간도 함께 기록되는 일이 있었다. 이럴 때마다 어느 정도의 시간을 기록된 시간에서 빼야할지 고민이 된다."
"5분에서 10분 정도의 짧은 휴식시간을 가질 때마다 타이머를 눌러서 중지하고 다시 눌러서 시작하는 것이 귀찮을 때가 있다."
위 의견을 바탕으로 캠스터디 서비스인데, 사용자의 자리 이탈을 실시간으로 인식해서 공부시간을 자동으로 기록하여 인증까지 해주는 서비스가 있으면 좋겠다는 생각을 했다.
'자리 이탈 감지 캠 스터디 관리 웹 서비스'를 구현하려면 크게 두 가지 기술 확인이 필요하다.
1. 실시간 영상통화
2. 캠에 보이는 손의 유무를 인식하여 스톱워치를 자동으로 중지할 수 있는지
먼저 WebRTC로 실시간 영상통화 웹 사이트를 구현해보자
구현한 영상통화 웹 사이트가 다음 두 조건을 만족하는지 확인해야한다.
npm init -y
terminal에 위 명령어로 package.json 파일을 생성한다.
dependency를 추가하자
npm i express ejs socket.io
ejs는 embedded javascript의 약자로 express에서 dynamic website를 만들기 위해 template로 사용되는 파일이다.
socket.io는 양방향 통신에 필요하다.
npm i uuid
uuid는 네트워크 상에서 고유성이 보장되는 id를 만들기 위한 고유 식별자이다.
영상통화의 개별적인 id를 부여해 room을 만들 때 사용된다.
위처럼 새로운 id를 새로고침할 때마다 부여해준다.
npm i --save-dev nodemon
nodemon(node monitor)는 파일이 수정되면 자동으로 노드를 재시작하는 확장 모듈이다. 노드 애플리케이션의 소스 코드를 수정할 때마다 매번 명령어로 새로 시작할 필요가 없어 편리하다.
package.json 파일의 script 부분에
"devStart" : "nodemon server.js"
을 추가하여 npm run devStart을 한번 하면 소스코드를 수정할때마다 새로 시작하지 않아도 된다.
bash terminal을 열어서 peer 라이브러리를 다운로드하자
peerjs
npm i -g peer
peerjs --port 3001
하면 3001 포트에서 PeerServer가 작동된다.
코드를 짜는 중에 아래와 같은 에러가 났다.
TypeError: Cannot read property 'emit' of undefined
(해결방법)
socket.io 버전이 상위버전이라 emit에서 에러가 났다.
socket.io 버전을 낮추자
npm install socket.io@^2.3.0
package.json 파일에서 socket.io 버전이 제대로 설치되었는지 확인.
package.json
{
"name": "webrtc_test_2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"devStart": "nodemon server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ejs": "^3.1.6",
"express": "^4.17.1",
"socket.io": "^2.3.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.15"
}
}
server.js
const express = require('express')
const app = express()
const server = require('http').Server(app) //socket.io는 http위에서 동작한다.
const io = require('socket.io')(server)
const { v4: uuidV4 } = require('uuid')
app.set('view engine', 'ejs')
app.use(express.static('public')) //모든 js코드는 public 폴더 안에 넣는다.
app.get('/', (req, res) => {
res.redirect(`/${uuidV4()}`)
//새로고침할때마다 랜덤 dynamic url을 만들어서 new room 생성
})
app.get('/:room', (req, res) => {
res.render('room', { roomId: req.params.room })
})
io.on('connection', socket => {
socket.on('join-room', (roomId, userId) => {
socket.join(roomId)
socket.to(roomId).broadcast.emit('user-connected', userId)
socket.on('disconnect', () => {
socket.to(roomId).broadcast.emit('user-disconnected', userId)
})
})
})
server.listen(3000)
room.ejs
room의 view를 보여주기위해 작성.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
const ROOM_ID = "<%= roomId %>"
</script>
<script
defer
src="https://unpkg.com/peerjs@1.3.1/dist/peerjs.min.js"
></script>
<script src="/socket.io/socket.io.js" defer></script>
<script src="script.js" defer></script>
<title>Document</title>
<style>
#video-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 300px);
grid-auto-rows: 300px;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<div id="video-grid"></div>
</body>
</html>
script.js
const socket = io('/')
const videoGrid = document.getElementById('video-grid')
/*
const myPeer = new Peer(undefined, {
host: '/',
port: '3001'
})
*/
const myPeer = new Peer();
const myVideo = document.createElement('video')
myVideo.muted = true
const peers = {}
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(stream => {
addVideoStream(myVideo, stream)
myPeer.on('call', call => {
call.answer(stream)
const video = document.createElement('video')
call.on('stream', userVideoStream => {
addVideoStream(video, userVideoStream)
})
})
socket.on('user-connected', userId => {
connectToNewUser(userId, stream)
})
})
socket.on('user-disconnected', userId => {
if (peers[userId]) peers[userId].close()
})
myPeer.on('open', id => {
socket.emit('join-room', ROOM_ID, id)
})
function connectToNewUser(userId, stream) {
const call = myPeer.call(userId, stream)
const video = document.createElement('video')
call.on('stream', userVideoStream => {
addVideoStream(video, userVideoStream)
})
call.on('close', () => {
video.remove()
})
peers[userId] = call
}
function addVideoStream(video, stream) {
video.srcObject = stream
video.addEventListener('loadedmetadata', () => {
video.play()
})
videoGrid.append(video)
}
(영상통화 웹 사이트 하나만 열었을 때)
(영상통화 웹 사이트 같은 url로 두개 열었을 때)
두 개 이상의 기기에서도 실시간 영상통화가 가능한지 구현해보자.
OpenSSL로 private.pem, public.pem 파일을 생성하자.
server.js 수정
const express = require('express');
const app = express();
const { v4: uuidV4 } = require('uuid');
const fs = require('fs');
const https = require('https');
const server = https.createServer(
{
key: fs.readFileSync('./public/private.pem'),
cert: fs.readFileSync('./public/public.pem'),
requestCert: false,
rejectUnauthorized: false,
},
app
);
const io = require('socket.io')(server);
app.set('view engine', 'ejs')
app.use(express.static('public'))
app.get('/', (req, res) => {
res.redirect(`/${uuidV4()}`)
})
app.get('/:room', (req, res) => {
res.render('room', { roomId : req.params.room })
})
io.on('connection', socket => {
socket.on('join-room', (roomId, userId) => {
socket.join(roomId)
socket.to(roomId).emit('user-connected', userId)
socket.on('disconnect', () => {
socket.to(roomId).emit('user-disconnected', userId)
})
})
})
app.get('/:room')
server.listen(3000)
https://서버IP:3000 으로 접속하자.
서버IP주소는 cmd창에서 다음 명령어로 확인하자.
ipconfig
PC에서 https://서버IP:3000 로 들어가게 되면 아래 같은 창이 뜬다.
고급 버튼을 누른다.
localhost(안전하지않음)으로 이동한다.
모바일도 마찬가지로 https://서버IP:3000 로 접속한다.
PC에서 볼때>
모바일에서 볼때>
잘 읽었어요~ 김현수교수