๊ด€๋ฆฌ ๋ฉ”๋‰ด

๐Ÿ–ฅ dev-ruby

[WebRTC] React+TypeScript+WebRTC ๊ฐœ๋…์ •๋ฆฌ + ๊ตฌํ˜„ํ•˜๊ธฐ ๋ณธ๋ฌธ

WebRTC

[WebRTC] React+TypeScript+WebRTC ๊ฐœ๋…์ •๋ฆฌ + ๊ตฌํ˜„ํ•˜๊ธฐ

ruby_s 2022. 11. 27. 04:49
728x90
๋ฐ˜์‘ํ˜•
SMALL

๋ถ€์ŠคํŠธ์บ ํ”„ ์›น๋ชจ๋ฐ”์ผ ๋ฉค๋ฒ„์‹ญ ๊ทธ๋ฃน ํ”„๋กœ์ ํŠธ์—์„œ WebRTC๋ฅผ ๋‹ค๋ฃจ๊ฒŒ ๋๋‹ค. ๋ณธ๊ฒฉ์ ์ธ ์ž‘์—…์— ๋“ค์–ด๊ฐ€๊ธฐ ์•ž์„œ WebRTC์— ๋Œ€ํ•ด ๋ฌด์ง€ํ•œ ์ƒํƒœ์˜€๊ธฐ์— ๋ฏธ๋ฆฌ ํ•™์Šต์„ ํ•ด๋ณด์•˜๋‹ค
์ฒ˜์Œ ํ•™์Šตํ•  ๋•Œ๋Š” ์ง„์งœ ์ด๊ฒŒ ๋ญ์ง€ ์‹ถ์—ˆ๋‹ค.. ๊ทธ๋ž˜์„œ ์ดํ•ด ๋ชปํ•œ ์ฑ„ ์ผ๋‹จ ๊ฐœ๋…์ด๋ผ๋„ ๋งˆ๊ตฌ ์ ์–ด๋†จ๋‹ค
MDN ๋ฌธ์„œ๋ฅผ ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด๊ณ , ๋ธ”๋กœ๊ทธ ์ž๋ฃŒ๋ฅผ ์—„์ฒญ ์ฐพ์•„ ๋Œ์•„ ๋Œ•๊ธฐ๋‹ค๋ณด๋‹ˆ ์–ด๋Š์ •๋„ ์ •๋ฆฌ๊ฐ€ ๋๋‹ค
ํ•˜๋‹จ์— ์ฐธ๊ณ ํ•œ ๋ฌธ์„œ๋“ค ์ ์–ด๋†จ์–ด์š” ๐Ÿค—

WebRTC(Web Real Time Communication)๋ž€??

๋ธŒ๋ผ์šฐ์ €์™€ ๋ชจ๋ฐ”์ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ณ„๋„์˜ ์†Œํ”„ํŠธ์›จ์–ด ์—†์ด ์Œ์„ฑ, ์˜์ƒ ๋ฏธ๋””์–ด, ํ…์Šค, ํŒŒ์ผ๊ณผ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋“ค์„ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ (RTC)์œผ๋กœ ์ฃผ๊ณ  ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ธฐ์ˆ ์ด๋‹ค.
Peer To Peer ๋ฐฉ์‹์œผ๋กœ ์ „์†กํ•˜๋ฉฐ, ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„ ํ•˜๋‚˜๋งŒ ์žˆ์œผ๋ฉด ๋œ๋‹ค.

์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋Š” ๋ญ”๋ฐ?

์„œ๋กœ ๋‹ค๋ฅธ ๋„คํŠธ์›Œํฌ์— ์žˆ๋Š” ๋””๋ฐ”์ด์Šค๋“ค๋ผ๋ฆฌ ํ†ต์‹ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ฐ์ž์˜ ์œ„์น˜๋ฅผ ์•Œ๊ณ , ๋ฏธ๋””์–ด ํฌ๋งท์„ ๋งž์ถœ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์ด๋Ÿฌํ•œ ๊ณผ์ •์„ ์‹œ๊ทธ๋„๋ง ์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.
๊ฐ ๋””๋ฐ”์ด์Šค๋“ค์€ ์„œ๋ฒ„๋กœ ๋ณธ์ธ์˜ ์ •๋ณด๋ฅผ ๋ณด๋‚ด๊ณ , ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋Š” ๋ฐ›์€ ๋„คํŠธ์›Œํฌ ์ •๋ณด๋ฅผ ๊ทธ๋Œ€๋กœ ์ƒ๋Œ€ ๋””๋ฐ”์ด์Šค์—๊ฒŒ ์ „๋‹ฌํ•ด์ค€๋‹ค.

์‹ค์‹œ๊ฐ„์ด๋ผ๋ฉด ์›น์†Œ์ผ“๊ณผ ๋ฌด์Šจ ์ฐจ์ด์ฃ ?

์›น์†Œ์ผ“์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ์ •๋ณด๋“ค์„ ์ „๋‹ฌ๋ฐ›๋Š”๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ WebRTC๋Š” ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ์„œ๋กœ์˜ ๋„คํŠธ์›Œํฌ ์ •๋ณด๋“ค์„ ๋ฐ›๋Š”๋‹ค. (์‹œ๊ทธ๋„ˆ๋ง ์„œ๋ฒ„๋Š” ๋‹จ์ˆœํžˆ ํด๋ผ์ด์–ธํŠธ์˜ ๋„คํŠธ์›Œํฌ ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•ด์ฃผ๋Š” ๋งค๊ฐœ์ž ์—ญํ• ์„ ํ•œ๋‹ค.)
ํด๋ผ์ด์–ธํŠธ๋Š” ์ƒ๋Œ€ ํด๋ผ์ด์–ธํŠธ์˜ ๋„คํŠธ์›Œํฌ ์ •๋ณด๋ฅผ ๋ฐ›๊ณ  “๋„ˆ ๊ฑฐ๊ธฐ ์žˆ๊ตฌ๋‚˜?” ๋ฅผ ์•Œ๊ฒŒ ๋˜๋ฉด ๊ทธ๋•Œ๋ถ€ํ„ฐ ํด๋ผ์ด์–ธํŠธ๋ผ๋ฆฌ ์—ฐ๊ฒฐ์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.


STUN๊ณผ TURN

์ด ๊ฐœ๋…์„ ์•Œ๊ธฐ ์ „์— NAT์ด๋ผ๋Š” ๊ฐœ๋…์„ ๋จผ์ € ์•Œ์•„์•ผ ํ•œ๋‹ค.

NAT(network Address Translation)์ด๋ž€?

์‚ฌ์„ค IP๋ฅผ ๊ณต์ธ IP๋กœ ๋ณ€๊ฒฝํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ ์ฃผ์†Œ ๋ณ€ํ™˜ ์„œ๋น„์Šค์ด๋‹ค.
→ ๋ผ์šฐํ„ฐ ๋“ฑ์˜ ์žฅ๋น„๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์ˆ˜์˜ ์‚ฌ์„ค IP๋ฅผ ๊ณต์ธ IP ์ฃผ์†Œ๋กœ ๋ณ€ํ™˜ํ•˜๊ฒŒ ๋œ๋‹ค.
๋ฌด์Šจ ๋ง์ด๋ƒ๋ฉด,
ํšŒ์‚ฌ๋ง/๋‚ด๋ถ€๋ง(LAN) ์€ Private IP์ด๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋„คํŠธ์›Œํฌ์—์„œ๋Š” ํ†ต์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.
๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๊ฐ€ ๋„คํŠธ์›Œํฌ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๊ธฐ ์œ„ํ•ด์„œ๋Š” Public IP ๊ฐ€ ํ•„์š”ํ•œ๋ฐ, NAT๋Š” Private IP๋ฅผ Public IP๋กœ 1๋Œ€1 ๋Œ€์‘์‹œ์ผœ ๋ณ€ํ™˜ํ•˜๋Š” ์žฅ์น˜๋‹ค.
๊ทธ๋ ‡๋‹ค๋ฉด STUN, TURN์€ ๋ญ˜๊นŒ
P2P๋กœ ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋„ ์—ญ์‹œ ์ƒ๋Œ€๋ฐฉ์˜ Public IP ์ฃผ์†Œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ํ•˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ๋Š” ๋ณดํ†ต ๋ฐฉํ™”๋ฒฝ์ด๋‚˜ NAT ๋’ค์—์„œ ๋ณดํ˜ธ๋ฐ›๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Public IP ์ฃผ์†Œ๋ฅผ ์•Œ๊ธฐ ์–ด๋ ต๋‹ค.
๊ทธ๋ž˜์„œ WebRTC์—์„œ๋Š” STUN ์„œ๋ฒ„ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•˜๊ณ  ์žˆ๋‹ค.
์„œ๋กœ๊ฐ„์˜ ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ์ •๋ณด๋ฅผ ๊ณต์œ ํ•˜์—ฌ P2P ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฒƒ์ด Stun/Turn Server์ด๋‹ค.

STUN์€ ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ํ”„๋กœํ† ์ฝœ์ด๋‹ค. STUN ํด๋ผ์ด์–ธํŠธ๋Š” ์‚ฌ์„ค๋ง(private network)์— ์œ„์น˜ํ•˜๊ณ , STUN ์„œ๋ฒ„๋Š” ์ธํ„ฐ๋„ท๋ง์— ์œ„์น˜ํ•œ๋‹ค. STUN ํด๋ผ์ด์–ธํŠธ๋Š” ์ž์‹ ์˜ ๊ณต์ธ IP ์ฃผ์†Œ๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด STUN ์„œ๋ฒ„์—๊ฒŒ ์š”์ฒญํ•˜๊ณ , STUN ์„œ๋ฒ„๋Š” STUN ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ๊ณต์ธ IP ์ฃผ์†Œ๋ฅผ ์‘๋‹ตํ•œ๋‹ค.

TURN ์„œ๋ฒ„๋Š” ์•„์ง ํ™•์‹คํ•˜๊ฒŒ ์ดํ•ด๊ฐ€
๋˜์ง€ ์•Š์•„์„œ ํ•˜๋‹จ์— ์ฒจ๋ถ€๋œ ๋…ธ์…˜ ์ƒ์—๋งŒ ์ ์–ด๋†จ๊ณ , ์—ฌ๊ธฐ์—” ์ ์ง€ ์•Š๊ฒ ๋‹ค. ๊ถ๊ธˆํ•˜๋ฉด ๋…ธ์…˜์œผ๋กœ ํƒ€๊ณ  ๋“ค์–ด๊ฐ€์„œ ๋ณผ ์ˆ˜ ์žˆ์–ด์š”

์‚ฌ์„ค๋ง(private network): ์ธํ„ฐ๋„ท ์–ด๋“œ๋ ˆ์‹ฑ ์•„ํ‚คํ…์ฒ˜์—์„œ ์‚ฌ์„ค IP ์ฃผ์†Œ ๊ณต๊ฐ„์„ ์ด์šฉํ•˜๋Š” ๋„คํŠธ์›Œํฌ

 

์–ด๋Š ์ •๋„ ์šฉ์–ด ์ •๋ฆฌ๋Š” ๋œ ๊ฒƒ ๊ฐ™์œผ๋‹ˆ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ๊ทธ๋„๋ง ๊ณผ์ •์„ ์„ค๋ช…ํ•ด๋ณด๊ฒ ๋‹ค

์ฒ˜์Œ์— ํ…์ŠคํŠธ๋กœ๋งŒ ์ ํ˜€์ง„ ๊ธ€์„ ๋ณด๊ณ  ๊ณต๋ถ€ํ–ˆ์—ˆ๋Š”๋ฐ, ์ด ์‚ฌ์ง„์„ ๋ณด๋‹ˆ๊นŒ ํ๋ฆ„์ด ์–ด๋Š ์ •๋„ ์ดํ•ด๊ฐ€ ๋์—ˆ๋‹ค


1. Session Descriptions ๊ตํ™˜ํ•˜๊ธฐ

  1. ๋จผ์ €, Peer A๊ฐ€ ์ด๋ฏธ ์ ‘์†ํ•ด ์žˆ์—ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž.
  2. ๊ทธ ํ›„ Peer B๊ฐ€ ๋“ค์–ด์˜ค๋ฉด, B๊ฐ€ A์—๊ฒŒ “๋‚˜ ๋“ค์–ด์™”์–ด~” ๋ผ๊ณ  ์•Œ๋ฆฐ๋‹ค.
  3. ๊ทธ๋Ÿผ A๋Š” createOffer()๋ฅผ ํ†ตํ•ด offer๋ฅผ ๋งŒ๋“ ๋‹ค. ์ด offer๋Š” ์œ„์—์„œ ๋งํ•œ SDP๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
  4. ๋‚ด SDP๋ฅผ setLocalDescription์„ ํ†ตํ•ด ๋กœ์ปฌ SDP๋กœ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
  5. ์ƒ์„ฑํ•œ SDP๋ฅผ ์ƒ๋Œ€ B์—๊ฒŒ ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ๋ณด๋‚ด์ค€๋‹ค.
  6. B๋Š” ์ „๋‹ฌ๋ฐ›์€ SDP๋ฅผ setRemoteDescription์„ ํ†ตํ•ด RemoteDescription ์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.
  7. B๋Š” ๋ฆฌ๋ชจํŠธ์— A์˜ SDP๋ฅผ ์„ค์ •ํ•˜๊ณ  ๋‚˜๋ฉด createAnswer()๋ฅผ ํ†ตํ•ด A์—๊ฒŒ ๋ณด๋‚ผ SDP๋ฅผ ๋˜‘๊ฐ™์ด ์ƒ์„ฑํ•œ๋‹ค.
    1. ์œ„์—์„œ ์ƒ์„ฑํ•œ createOffer์™€ ๊ฐ™์€ ๊ณผ์ •์ด๋‹ค.
  8. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ A์—๊ฒŒ Answer๋ฅผ ๋ณด๋‚ด์ฃผ๊ณ , A๋Š” ๋ฐ›์€ answer๋ฅผ RemoteDescription ์œผ๋กœ ์„ค์ •ํ•˜๊ฒŒ ๋œ๋‹ค

 

2. Ice Candidate ๊ตํ™˜ํ•˜๊ธฐ

SDP๋ฅผ ์„œ๋กœ ๊ตํ™˜ํ•œ ํ›„์—, ๋‘ ํ”ผ์–ด๋“ค์€ ICE candidate (ICE ํ›„๋ณด)๋“ค์„ ๊ตํ™˜ํ•˜๊ธฐ ์‹œ์ž‘ํ•œ๋‹ค.
์ƒ๋Œ€์™€ ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๋Š” candidate๋ฅผ ์ฐพ๊ธฐ ์‹œ์ž‘ํ•˜๋Š” ๊ณผ์ •์ด๋‹ค.

  1. ๊ฐ ํด๋ผ์ด์–ธํŠธ๋Š” ํ˜„์žฌ ๋‚ด ๋„คํŠธ์›Œํฌ ์ •๋ณด๊ฐ€ ํ™•๋ณด๋˜๋ฉด ์‹คํ–‰๋  callback์„ onicecandidate ํ•ธ๋“ค๋Ÿฌ์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค. ๋‘๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.
peerConnection.addEventListener('icecandidate', (e) => {});
peerConnection.onicecandidate = (e) => {};
  1. ๋‚ด ๋„คํŠธ์›Œํฌ ์ •๋ณด๊ฐ€ ํ™•๋ณด๋˜๋ฉด ์‹œ๊ทธ๋„๋ง ์„œ๋ฒ„๋ฅผ ํ†ตํ•ด ์ƒ๋Œ€ Peer์—๊ฒŒ ๋‚ด ice candidate๋ฅผ ์ „์†กํ•œ๋‹ค.
  2. ์ƒ๋Œ€ Peer์˜ ice candidate๊ฐ€ ๋„์ฐฉํ•˜๋ฉด, ํด๋ผ์ด์–ธํŠธ์— ์ €์žฅํ•ด๋‘์—ˆ๋˜ ์ƒ๋Œ€ Peer Connection์„ ์ฐพ์•„์„œ ์ƒ๋Œ€์˜ addIceCandidate() ๋กœ candidate๋ฅผ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
    1. ์ด ์ž‘์—…์€ ์„œ๋กœ๊ฐ€ ๋ชจ๋“  IceCandidate๋ฅผ ๊ตํ™˜ํ•  ๋•Œ๊นŒ์ง€ ์ง„ํ–‰๋œ๋‹ค.
  3. IceCandidate๋ฅผ ๋ชจ๋‘ ๊ตํ™˜ํ•˜๊ณ  ๋‚˜๋ฉด, addEventListener์˜ track ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ์ƒ๋Œ€์˜ stream์„ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
peerConnection.addEventListener('track', (e) => {});
peerConnection.ontrack = (e) => {};

 

ํ๋ฆ„์€ ์—ฌ๊ธฐ๊นŒ์ง€! ์ด์ œ ์‹ค์ „์œผ๋กœ ๋“ค์–ด๊ฐ€์„œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.
์‚ฌ์‹ค ํ๋ฆ„๋งŒ ์ฝ์—ˆ์„ ๋•Œ๋Š” ์ด๊ฒŒ ๋ฌด์Šจ ๋ง์ธ๊ฐ€ ์‹ถ์„์ˆ˜๋„ ์žˆ๋Š”๋ฐ, ์—ญ์‹œ ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๋ฉด์„œ ๋ฐฐ์šฐ๋Š”๊ฒŒ ์ข‹์€ ๊ฒƒ ๊ฐ™๋‹ค.

 

Server ์ฝ”๋“œ

import { Server } from 'socket.io';

function signalingSocketServer(io: Server) {
  const signaling = io.of('namespace');

  signaling.on('connection', (socket) => {
    socket.on('join', () => {
      const senderId = socket.id;
      socket.broadcast.emit('join', senderId);
    });

    // senderId : offer๋ฅผ ๋ณด๋‚ด๊ธฐ ์‹œ์ž‘ํ•œ ์‚ฌ๋žŒ์˜ id, receiveId : offer๋ฅผ ๋ฐ›๋Š” ์‚ฌ๋žŒ์˜ id
    socket.on('offer', ({ receiveId, offer }) => {
      const senderId = socket.id;
      socket.to(receiveId).emit('offer', { senderId, offer });
    });

    socket.on('answer', ({ receiveId, answer }) => {
      const senderId = socket.id;
      socket.to(receiveId).emit('answer', { senderId, answer });
    });

    socket.on('ice-candidate', ({ receiveId, candidate }) => {
      const senderId = socket.id;
      socket.to(receiveId).emit('ice-candidate', { senderId, candidate });
    });

    socket.on('disconnect', () => {
      const senderId = socket.id;
      socket.broadcast.emit('disconnected', senderId);
    });
  });
}

export default signalingSocketServer;

์œ„์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ A๊ฐ€ ์žˆ๋Š” ์ƒํ™ฉ์—์„œ B๊ฐ€ ๋“ค์–ด์˜จ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ž.
join
B๊ฐ€ ๋“ค์–ด์˜ค๋ฉด socket.broadcast๋ฅผ ํ†ตํ•ด ์ด์ „์ด ์ด๋ฏธ ์ ‘์†ํ•ด ์žˆ๋˜ ๋ฉค๋ฒ„๋“ค์—๊ฒŒ B์˜ ์†Œ์ผ“ ์•„์ด๋””๋ฅผ ์•Œ๋ ค์ค€๋‹ค.
socket.broadcast : ๋‚˜ ์ž์‹ ์„ ์ œ์™ธํ•œ ๋ชจ๋‘์—๊ฒŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋ƒ„
https://socket.io/docs/v3/broadcasting-events/

offer
A๊ฐ€ B์—๊ฒŒ ๋ณด๋‚ธ offer๋ฅผ ๋ฐ›์•„์„œ socket.to ๋ฅผ ํ†ตํ•ด B์—๊ฒŒ๋งŒ offer๋ฅผ ์ „๋‹ฌํ•ด์ค€๋‹ค.
ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ›์„ ์‚ฌ๋žŒ์˜ Id๋ฅผ ๋ณด๋‚ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

answer
B๊ฐ€ ์ƒ์„ฑํ•œ answer๋ฅผ ๋ฐ›์•„์„œ socket.to ๋ฅผ ํ†ตํ•ด A์—๊ฒŒ๋งŒ ๋ณด๋‚ด์ค€๋‹ค.
๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ›์„ ์‚ฌ๋žŒ์˜ Id๋ฅผ ๋ณด๋‚ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

ice-candidate
์ด ๊ณผ์ •์€ A์™€ B ๋ชจ๋‘ ๋ณด๋‚ด๊ฒŒ ๋œ๋‹ค. icecandidate ์ด๋ฒคํŠธ๋กœ ๋“ฑ๋กํ•ด๋†“์€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์•ˆ์—์„œ ์ด ์†Œ์ผ“ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ „๋‹ฌ๋ฐ›์€ candidate๋ฅผ ๊ฐ๊ฐ ์ƒ๋Œ€๋ฐฉ์—๊ฒŒ ๋ณด๋‚ธ๋‹ค.
์„œ๋ฒ„๋Š” ๋‹จ์ง€ ์‹œ๊ทธ๋„๋ง์„ ์œ„ํ•œ ์šฉ๋„์ด๊ธฐ ๋•Œ๋ฌธ์— ๋งค์šฐ ๊ฐ„๋‹จํ•˜๋‹ค.


Client ์ฝ”๋“œ

const socket = io(env.SERVER_PATH + 'namespace');

const [participants, setParticipants] = useState<Map<string, MediaStream>>(new Map(),);
const myStreamRef = useRef<MediaStream | null>(null);
const myVideoRef = useRef<HTMLVideoElement | null>(null);
const peerConnectionRef = new Map();

const setMyStream = async () => {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: true,
  });
  myStreamRef.current = stream;

  if (myVideoRef.current) {
    myVideoRef.current.srcObject = myStreamRef.current;
  }
};

/**
 * Peer์™€ ์—ฐ๊ฒฐํ•˜๊ธฐ
 * @param peerId ์—ฐ๊ฒฐํ•  ํ”ผ์–ด์˜ Id
 * @returns ์ƒˆ๋กœ ์ƒ์„ฑํ•œ peerConnection ๊ฐ์ฒด
 */
const setPeerConnection = (peerId: string) => {
  const peerConnection = new RTCPeerConnection({
	iceServers: [
	  {
	    urls: ['stun:stun.l.google.com:19302'],
	  },
	],
  });

  /* ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ: Peer์—๊ฒŒ candidate๋ฅผ ์ „๋‹ฌ ํ•  ํ•„์š”๊ฐ€ ์žˆ์„๋•Œ ๋งˆ๋‹ค ๋ฐœ์ƒ */
  peerConnection.addEventListener('icecandidate', (e) => {
    const candidate = e.candidate;
    if (!candidate) return;

    socket.emit(RTC_MESSAGE.ICE_CANDIDATE, {
      receiveId: peerId,
      candidate,
    });
  });

  /* ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ: peerConnection์— ์ƒˆ๋กœ์šด ํŠธ๋ž™์ด ์ถ”๊ฐ€๋์„ ๊ฒฝ์šฐ ํ˜ธ์ถœ๋จ */
  peerConnection.addEventListener('track', (e) => {
    if (participants.has(peerId)) {
      return;
    }

    const [peerStream] = e.streams;

    // ์ƒˆ๋กœ์šด peer๋ฅผ ์ฐธ์—ฌ์ž์— ์ถ”๊ฐ€
    setParticipants((prev) => {
      const newState = new Map(prev);
      newState.set(peerId, peerStream);
      return newState;
    });
  });

  myStreamRef.current?.getTracks().forEach((track) => {
    if (!myStreamRef.current) return;

    // ๋‹ค๋ฅธ ์œ ์ €์—๊ฒŒ ์ „๋‹ฌํ•ด์ฃผ๊ธฐ ์œ„ํ•ด ๋‚ด ๋ฏธ๋””์–ด๋ฅผ peerConnection ์— ์ถ”๊ฐ€ํ•œ๋‹ค.
    // track์ด myStreamRef.current(๋‚ด ์ŠคํŠธ๋ฆผ)์— ์ถ”๊ฐ€๋จ
    peerConnection.addTrack(track, myStreamRef.current);
  });

  return peerConnection;
};

useEffect(() => {
  setMyStream();

  /* ์œ ์ € join */
  socket.emit(RTC_MESSAGE.JOIN);

  /* ์ƒˆ๋กœ ๋“ค์–ด์˜จ ์œ ์ €์˜ socketId๋ฅผ ๋ฐ›์Œ */
  socket.on(RTC_MESSAGE.JOIN, async (socketId) => {
    const peerConnection = setPeerConnection(socketId);

    peerConnectionRef.set(socketId, peerConnection);

    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);

    socket.emit(RTC_MESSAGE.OFFER, {
      receiveId: socketId,
      offer,
    });
  });

  /* offer ๋ฐ›๊ธฐ */
  socket.on(RTC_MESSAGE.OFFER, async ({ senderId, offer }) => {
    const peerConnection = setPeerConnection(senderId);

    peerConnectionRef.set(senderId, peerConnection);

    await peerConnection.setRemoteDescription(offer);

    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);

    /* answer ์ „์†ก */
    socket.emit(RTC_MESSAGE.ANSWER, { receiveId: senderId, answer });
  });

  /* answer ๋ฐ›๊ธฐ */
  socket.on(RTC_MESSAGE.ANSWER, async ({ senderId, answer }) => {
    const peerConnection = peerConnectionRef.get(senderId);
    if (!peerConnection) {
      return console.log('Peer Connection does not exist');
    }

    await peerConnection.setRemoteDescription(answer);
  });

  /* ice candidate */
  socket.on(RTC_MESSAGE.ICE_CANDIDATE, ({ senderId, candidate }) => {
    const peerConnection = peerConnectionRef.get(senderId);
    if (!peerConnection) {
      return console.log('Peer Connection does not exist');
    }

    peerConnection.addIceCandidate(candidate);
  });

  /* disconnected */
  socket.on(RTC_MESSAGE.DISCONNECTED, (senderId) => {
    peerConnectionRef.delete(senderId);

    setParticipants((prev) => {
      const newState = new Map(prev);
      newState.delete(senderId);
      return newState;
    });
  });

  return () => {
    socket.off(RTC_MESSAGE.JOIN);
    socket.off(RTC_MESSAGE.OFFER);
    socket.off(RTC_MESSAGE.ANSWER);
    socket.off(RTC_MESSAGE.ICE_CANDIDATE);
    socket.off(RTC_MESSAGE.DISCONNECTED);
  };
}, []);

const streams = Array.from(participants.values());

return (
    <ul>
      {streams.map((stream) => (
        <li key={stream.id}>
           <Video stream={stream} />
        </li>
      ))}
    </ul>
);

๋ชจ๋“ˆํ™”๋Š” ์ž˜ ์•ˆ๋ผ์žˆ๋Š”๋ฐ ๋ฆฌํŒฉํ† ๋ง์„ ๋‹ค์‹œ ํ•ด์•ผํ•œ๋‹ค ใ…Žใ…Ž..
์‚ฌ์‹ค ์ด ์ฝ”๋“œ๋Š” ๋ฒ„๊ทธ๊ฐ€ ์ข€ ์žˆ๋‹ค.. ํ˜„์žฌ ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•œ ์œ„์น˜๋Š” ์ฐพ์•˜์œผ๋‚˜ ์›์ธ์€ ์ฐพ์ง€ ๋ชปํ•œ ์ƒํ™ฉ์ด๋‹ค. offer, answer, candidate ๊ณผ์ •์„ ํ†ตํ•ด SDP๋ฅผ ๋ชจ๋‘ ์„ค์ •์€ ํ•ด์ฃผ์—ˆ์œผ๋‚˜ peerConnection.addEventListener('track', (e) => {}) ์ด ๋ถ€๋ถ„์ด ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ๋‹ค..
๋‚ด๊ฐ€ ์ดํ•ดํ•œ ๋ฐ”๋กœ๋Š” track ์ด๋ฒคํŠธ๋Š” peerConnection.addTrack ๊ฐ€ ์‹คํ–‰๋  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์ƒ๋Œ€๋ฐฉ์ด peerConnection ์— ๋ณธ์ธ์˜ ์ƒˆ๋กœ์šด track์„ ์ถ”๊ฐ€ํ•˜๊ฒŒ ๋˜๋ฉด track ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜๊ณ , ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์‹คํ–‰๋˜๋ฉด์„œ ์ƒ๋Œ€์˜ stream์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.
ํ•˜์ง€๋งŒ track์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€๋œ ๊ฒƒ์„ ํ™•์ธํ–ˆ์œผ๋‚˜ ๊ฐ€๋” ํ•ธ๋“ค๋Ÿฌ์— ๋“ค์–ด๊ฐ€์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋”๋ผ.. ๋งŒ์•ฝ ๋ฒ„๊ทธ๋ฅผ ๋ฐœ๊ฒฌํ•˜์‹  ๋ถ„์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€๋กœ ๊ผญ๊ผญ ์•Œ๋ ค์ฃผ์„ธ์š” ใ… ใ…  ๐Ÿ™


์•„๋ฌดํŠผ ๋ฒ„๊ทธ๊ฐ€ ์žˆ๋”๋ผ๋„ ํ๋ฆ„์€ ๋งž์œผ๋‹ˆ๊นŒ ๋‹ค์‹œ ์„ค๋ช…์œผ๋กœ ๋Œ์•„๊ฐ€์„œ..

setMyStream

๋‚ด ๋น„๋””์˜ค ์ŠคํŠธ๋ฆผ์„ ์„ค์ •ํ•˜๋Š” ํ•จ์ˆ˜๋‹ค. myStreamRef ์—๋Š” MediaStream ํƒ€์ž…์ธ ์ŠคํŠธ๋ฆผ์„ ๊ฑธ์–ด์ฃผ๊ณ 
myVideoRef ์—๋Š” myVideoRef.current.srcObject ์— myStreamRef์˜ ์ŠคํŠธ๋ฆผ์„ ๋„ฃ์–ด์ฃผ๋ฉด, video ํƒœ๊ทธ์— ์•„๋ž˜์ฒ˜๋Ÿผ ๊ฑธ์–ด์ฃผ๋ฉด ๋‚ด ๋น„๋””์˜ค๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

<video ref={myVideoRef} />

 

setPeerConnection

Peer Connection์„ ์„ค์ •ํ•ด์ฃผ๋Š” ํ•จ์ˆ˜๋‹ค.

  1. ๋จผ์ €, new RTCPeerConnection ๋กœ ์ƒˆ๋กœ์šด ํ”ผ์–ด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๊ณ , ์Šคํ„ด ์„œ๋ฒ„๋ฅผ ์„ค์ •ํ•ด์ค€๋‹ค. ๊ตฌ๊ธ€์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค.
  2. icecandidate ์ด๋ฒคํŠธ๋ฅผ ๋“ฑ๋กํ•ด์ค€๋‹ค. ์ด ์ด๋ฒคํŠธ๋ฆฌ์Šค๋„ˆ๋Š” peer connection์ด candidate๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค๊ณ  ํŒ๋‹จ๋  ๋•Œ๋งˆ๋‹ค ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.
    1. e.candidate ๋กœ candidate๋ฅผ ๋ฐ›์•„์˜จ ํ›„, ์†Œ์ผ“ emit ์ด๋ฒคํŠธ๋ฅผ ๊ฑธ์–ด์ค€๋‹ค. ๋‚ด candidate๋ฅผ ์ƒ๋Œ€์—๊ฒŒ ๋ณด๋‚ด๋Š” ๊ณผ์ •์ด๋‹ค.
  3. track ์ด๋ฒคํŠธ๋ฅผ ๋“ฑ๋กํ•ด์ค€๋‹ค. ์ด ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋Š” ์œ„์—์„œ ๋งํ–ˆ๋˜ ๊ฒƒ์ฒ˜๋Ÿผ ์ƒ๋Œ€๋ฐฉ์ด peerConnection ์— ๋ณธ์ธ์˜ ์ƒˆ๋กœ์šด track์„ ์ถ”๊ฐ€ํ•˜๊ฒŒ ๋˜๋ฉด ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.
    1. e.streams๋กœ ์ƒ๋Œ€์˜ stream์„ ๋ฐ›์•„์˜จ ํ›„, UI์— ๋ฟŒ๋ ค์ฃผ๊ธฐ ์œ„ํ•ด useState๋กœ ๊ด€๋ฆฌ์ค‘์ธ participants์— ์ƒˆ๋กœ์šด ์ŠคํŠธ๋ฆผ์œผ๋กœ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
  4. getTracks() : ์ข…๋ฅ˜์— ๊ด€๊ณ„์—†์ด ์ŠคํŠธ๋ฆผ์˜ ํŠธ๋ž™ set์— ์žˆ๋Š” ๋ชจ๋“  MediaStreamTrack ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
    1. ๋‹ค๋ฅธ ์œ ์ €์—๊ฒŒ ๋‚ด ์Šคํฌ๋ฆผ์„ ์ „๋‹ฌํ•ด์ฃผ๊ธฐ ์œ„ํ•ด peerConnection์— ๋‚ด ์ŠคํŠธ๋ฆผ์„ addTrack์œผ๋กœ ๋‹ด๋Š”๋‹ค.

 

์ด์ œ useEffect ์•ˆ์œผ๋กœ ๋“ค์–ด์™€๋ณด์ž.

  1. ๋จผ์ € setMyStream ์„ ํ˜ธ์ถœํ•ด ๋‚ด ๋ฏธ๋””์–ด๋ฅผ ๋จผ์ € ์„ค์ •ํ•ด์ค€๋‹ค.
  2. join ์ด๋ฒคํŠธ๋กœ ๋‚ด๊ฐ€ ๋“ค์–ด์™”๋‹ค๋Š” ๊ฒƒ์„ ์ด๋ฏธ ์ ‘์†ํ•ด ์žˆ๋˜ ๋ชจ๋“  ์œ ์ €๋“ค์—๊ฒŒ ์•Œ๋ฆฐ๋‹ค.
  3. socket.on(RTC_MESSAGE.JOIN) : ๊ทธ๋Ÿผ ๋ˆ„๊ตฐ๊ฐ€๋Š” ์ด ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๊ฒŒ ๋  ๊ฒƒ์ด๋‹ค.
    1. ๋ˆ„๊ตฐ๊ฐ€ ๋“ค์–ด์™”์œผ๋‹ˆ ์ƒˆ๋กœ์šด peer connection์„ ์ƒ์„ฑํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
    2. peerConnectionRef์— socketId๋ฅผ ํ‚ค ๊ฐ’์œผ๋กœ peer connection์„ ์ €์žฅํ•ด๋‘์ž. socketId์˜ ์ƒ๋Œ€์™€ ๋งบ์€ connection์ด ์ด๊ฑฐ๋‹ค! ๋ผ๋Š” ๊ฒƒ์„ ๊ตฌ๋ถ„ํ•ด์ฃผ๊ธฐ ์œ„ํ•จ์ด๋‹ค.
    3. answer์™€ candidate๋ฅผ ๋ฐ›์„ ๋•Œ, ์ƒ๋Œ€์™€ ๋งบ์€ peer connection์— ๋ฐ›์€ answer์™€ candidate๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— peerConnection์„ ์ €์žฅํ•ด๋‘˜ ํ•„์š”๊ฐ€ ์žˆ๋‹ค.
  4. socket.on(RTC_MESSAGE.OFFER) : ์ƒ๋Œ€๋ฐฉ์˜ offer๋ฅผ ๋ฐ›๋Š” ๋ถ€๋ถ„์ด๋‹ค.
    1. ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฒƒ์€, offer๋ฅผ ๋ฐ›์œผ๋ฉด ์ƒˆ๋กœ์šด peerConnection์„ ์ƒ์„ฑํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
    2. Joinํ•  ๋•Œ ์ƒ์„ฑํ•œ peer connection์€ A์˜ ๊ฒƒ์ด๊ณ , offer๋ฅผ ๋ฐ›๋Š” ์ด๋ฒคํŠธ๋Š” B๊ฐ€ ๋ฐ›์„ ๊ฒƒ์ด๋ฏ€๋กœ B๋„ peerConnection์„ ์ƒ์„ฑํ•ด์•ผ ํ•œ๋‹ค
      1. → Peer๋Š” 1๋Œ€1๋กœ ๋งบ๋Š” ๊ฑฐ๊ธฐ ๋•Œ๋ฌธ์— A,B,C๊ฐ€ ์žˆ์„ ๋•Œ A-B, A-C, B-C ์ด๋ ‡๊ฒŒ Peer Connection์ด ์ƒ์„ฑ๋˜์–ด์•ผ ํ•œ๋‹ค. Peer Connection ๊ฐ์ฒด๋Š” ์‚ฌ์‹ค ์ƒ 6๊ฐœ์ธ ์…ˆ์ด๋‹ค.
      2. ๊ฐ์ž์˜ Peer Connection์— ์ƒ๋Œ€์˜ ์ •๋ณด๋ฅผ ์ €์žฅํ•ด๋‘์–ด์•ผ ํ•˜๋ฏ€๋กœ..
    3. ์ด์ œ, setRemoteDescription ๋กœ ๋ฐ›์€ offer ๋ฅผ ๋ฆฌ๋ชจํŠธ์— ๋„ฃ์–ด๋‘”๋‹ค.
    4. ์ด์ œ ์ „๋‹ฌํ•ด์ค„ answer๋ฅผ ์ƒ์„ฑํ•˜๊ณ  answer๋Š” ๋‚ด ๊ฒƒ์ด๋ฏ€๋กœ local description์— ์„ค์ • ํ›„ socket์˜ answer ์ด๋ฒคํŠธ๋กœ ๋ณด๋‚ด์ค€๋‹ค.
  5. socket.on(RTC_MESSAGE.ANSWER) : ์ƒ๋Œ€๋ฐฉ์˜ answer๋ฅผ ๋ฐ›๋Š” ๋ถ€๋ถ„์ด๋‹ค.
    1. ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ณด๋‚ธ ์‚ฌ๋žŒ์˜ socketId๋ฅผ ๋ฐ›์•„์•ผ ํ•œ๋‹ค. ๊ธฐ์กด์— on(’join’) ์—์„œ ์ €์žฅํ•ด๋‘์—ˆ๋˜ peer connection์—์„œ socketId๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ˆ„๊ตฌ์™€ ๋งบ์€ ์ปค๋„ฅ์…˜์ธ์ง€ ์ฐพ์€ ํ›„, ๊ทธ ์ปค๋„ฅ์…˜์— ๋ฆฌ๋ชจํŠธ๋กœ answer๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
  6. socket.on(RTC_MESSAGE.ICE_CANDIDATE) : ์ƒ๋Œ€๋ฐฉ์˜ candidate๋ฅผ ๋ฐ›๋Š” ๋ถ€๋ถ„์ด๋‹ค.
    1. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ณด๋‚ธ ์‚ฌ๋žŒ์˜ socketId๋ฅผ ๋ฐ›๊ณ , ๋ˆ„๊ตฌ์™€ ๋งบ์€ ์ปค๋„ฅ์…˜ ์ธ์ง€ ์ฐพ์€ ํ›„ addIceCandidate ๋กœ candidate๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
  7. socket.on(RTC_MESSAGE.DISCONNECTED) : ์ƒ๋Œ€๋ฐฉ์ด ๋‚˜๊ฐ”์„ ๊ฒฝ์šฐ peer connection๊ณผ participants์—์„œ ์ƒ๋Œ€๋ฐฉ์„ ์‚ญ์ œํ•œ๋‹ค.

 

๋‚ด๊ฐ€ ์ดํ•ดํ•œ ๋‚ด์šฉ์œผ๋กœ ์ฃผ์ €๋ฆฌ์ฃผ์ €๋ฆฌ ์ •๋ฆฌํ•ด๋ณด์•˜๋Š”๋ฐ ๋‚˜๋งŒ ์•„๋Š” ์–ธ์–ด๋กœ ์ •๋ฆฌํ•  ๊ฑฐ ๊ฐ™๊ธฐ๋„ ํ•˜๊ณ .. ๋‚˜๋ฆ„ ๋‚˜์ค‘์— ๋ด๋„ ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก ์ž์„ธํ•˜๊ฒŒ ์ ์–ด๋†จ๋‹ค ..!๐Ÿ˜Š
๋…ธ์…˜์— ์ •๋ฆฌํ•œ ๋งํฌ๋„ ์ฒจ๋ถ€ํ•œ๋‹ค.
https://saber-ash-4ab.notion.site/React-TypeScript-WebRTC-1defea4ed489417fb4ce9c7a047291f7

 

React+TypeScript+WebRTC ๊ตฌํ˜„ํ•˜๊ธฐ

๋ถ€์ŠคํŠธ์บ ํ”„ ์›น๋ชจ๋ฐ”์ผ ๋ฉค๋ฒ„์‹ญ ๊ทธ๋ฃน ํ”„๋กœ์ ํŠธ์—์„œ WebRTC๋ฅผ ๋‹ค๋ฃจ๊ฒŒ ๋๋‹ค. ๋ณธ๊ฒฉ์ ์ธ ์ž‘์—…์— ๋“ค์–ด๊ฐ€๊ธฐ ์•ž์„œ WebRTC์— ๋Œ€ํ•ด ์ „ํ˜€ ๋ฌด์ง€ํ•œ ์ƒํƒœ์˜€๊ธฐ์— ๋ฏธ๋ฆฌ ํ•™์Šต์„ ํ•ด๋ณด์•˜๋‹ค

saber-ash-4ab.notion.site

 

๋ ˆํผ๋Ÿฐ์Šค

https://dareun.github.io/webRTCแ„…แ…ณแ†ฏ-แ„‹แ…ตแ„‹แ…ญแ†ผแ„’แ…กแ†ซ-แ„’แ…ชแ„‰แ…กแ†ผแ„’แ…ฌแ„‹แ…ด-แ„€แ…ขแ„‡แ…กแ†ฏ
https://velog.io/@gojaegaebal/210307-๊ฐœ๋ฐœ์ผ์ง€90์ผ์ฐจ-์ •๊ธ€-๋‚˜๋งŒ์˜-๋ฌด๊ธฐ-ํ”„๋กœ์ ํŠธ-WebRTC๋ž€-๋ฌด์—‡์ธ๊ฐ€2-ICE-SDP-Signalling
https://medium.com/@hyun.sang/webrtc-webrtc๋ž€-43df68cbe511
https://kbs77.tistory.com/102
https://surprisecomputer.tistory.com/14
https://chuun92.tistory.com/39
https://web.dev/webrtc-infrastructure/

728x90
๋ฐ˜์‘ํ˜•
LIST