준비된 것
- HTTPS (
https://{아이디}.zyo.kr) —getUserMedia필수 조건 - 카메라·마이크·화면공유: 같은 출처(
self)에서 허용 - 시그널링:
wss://{아이디}.zyo.kr/ws(계정·방 단위 중계) - STUN 목록:
GET /wsJSON의iceServers
curl -sS https://아이디.zyo.kr/ws
Developer · Hosting · WebRTC
교육생 사이트는 HTTPS + 카메라/마이크 권한 + 시그널링 웹소켓이 준비되어 있습니다. 영상·음성·데이터채널은 브라우저끼리 P2P로 주고받고, 연결을 맺는 신호만 서버 웹소켓으로 전달합니다.
https://{아이디}.zyo.kr) — getUserMedia 필수 조건self)에서 허용wss://{아이디}.zyo.kr/ws (계정·방 단위 중계)GET /ws JSON의 iceServerscurl -sS https://아이디.zyo.kr/ws
이 페이지(포털)와 교육생 사이트에서 카메라가 켜지는지 확인합니다. 브라우저가 권한을 물으면 허용하세요.
WebRTC 미디어는 서버를 거치지 않습니다. offer / answer / ICE candidate 만 웹소켓으로 교환합니다.
wss://아이디.zyo.kr/ws/call 에 두 탭(또는 두 PC)이 접속createOffer → JSON으로 sendsetRemoteDescription 후 createAnswericecandidate 도 같은 방으로 전달const info = await fetch('/ws').then((r) => r.json());
const pc = new RTCPeerConnection({ iceServers: info.iceServers });
const ws = new WebSocket(`wss://${location.host}/ws/call`);
const local = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
local.getTracks().forEach((t) => pc.addTrack(t, local));
pc.onicecandidate = (ev) => {
if (ev.candidate) ws.send(JSON.stringify({ type: 'ice', candidate: ev.candidate }));
};
pc.ontrack = (ev) => {
remoteVideo.srcObject = ev.streams[0];
};
ws.onmessage = async (ev) => {
const msg = JSON.parse(ev.data);
if (msg.type === 'offer') {
await pc.setRemoteDescription(msg.offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({ type: 'answer', answer }));
} else if (msg.type === 'answer') {
await pc.setRemoteDescription(msg.answer);
} else if (msg.type === 'ice' && msg.candidate) {
await pc.addIceCandidate(msg.candidate);
}
};
async function call() {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: 'offer', offer }));
}
웹소켓은 보낸 사람도 메시지를 받습니다. 자기 offer를 다시 처리하지 않도록 호출자 구분 필드를 넣으세요.
allow="camera; microphone; display-capture" 가 필요합니다.