socket
webrtc 연결 과정
start를 통해 socket open, message, close, error 를 listen 상태로 유지
handlePerrConnection
- createPeerConnection
- new RTCPeerConnection을 통해 myPeerConnection 설정
- handleICECandidateEvent
- sendToServer를 통해 ice data전송
- handleTrackEvent : 해당 함수를 통해서 얻어온 정보를 video tag의 srcObject 로 연결해준다?
- handleICECandidateEvent
- new RTCPeerConnection을 통해 myPeerConnection 설정
- getMedia
- 기존의 track을 stop
- 미디어 장치를 얻어옴
- myPeerConnection에 track, localStream 추가
- handleNegoriationNeededEvent
- (createOffer).then(setLocalDescription).then(sendToServer(Offer)
<aside>
💡 WebRTC samples → 이것 저것 테스트 해볼수 있는 DOCS 사이트
</aside>
webrtc Docs translate
Session descriptions
webrtc에서 endpoint의 구성은 session descriptoin이라 불린다. 해당 description은 보내지는 media의 종류, 이것의 format, 사용되는 protocol, endpoint’s의 ip, port number, 그리고 미디어 전달 endpoint를 설명하기 위해 필요한 정보들이 포함되어 있다. 이러한 정보들은 session description protocol(aka SDP)를 통해 전달되고 저장된다. 만약 SDP format에 더 자세히 알고싶으면 RFC 8866 을 찾아봐라
유저가 다른유저를 부르기 위해 WebRTC르르 실행할 때, offer라고 불리는 description을 만들어낸다. 해당 description 은 caller 가 제안한 통신설정에 대한 모든 정보가 포함된다. 해당 정보를 받는 사람은 그들의 통화 종단에 대한 설명인 answer를 응답한다. 이 경우에 양쪽 장비들은 media data를 교환하기 위해 필요한 정보를 서로 공유한다. 이러한 교환은 Interactive Connectivity Establishment(aka ICE)를 통해 관릴된다. ICE는 두 장치가 비록 Network Address Translation(aka NAT)에 의해 분리되어있더라도 중개인에 의해 offer 과 answer 을 교환하는 protocol이다.
그러면 각각의 peer은 두 description 은 유지한다: 본인에 대한 description 인 local description, 다른 통신 종단점에 대한 설명인 remote description
offer/answer process는 통신이 처음 설정됬을 때 뿐만 아니라, peer들의 구성에 변경이 필요할때 수행된다. 새로운 call 이던지 존제하던것의 재구성이던지 관계없이 offer/answer 을 교환하기 위한기본적인 과정이다. 일단 ICE 계층은 제외하고 설명함
- caller는 MediaDevices.getMedia를 통해 caller의 장치에서 local Media를 발견한다.
- caller 는 PRCPeerConnection을 만들고 PTCPeerConnection.addTrack()을 요청한다
- PRCPeerConnection.createOffer()을 통해 offer 를 만든다
- 해당 offer(SDP) 를 PTCPeerConnection.setLocalDescription() 을 호출해 local-description에 저장한다.
- setLocalDescription 후에, ice 후보를 생성하기위해 stun server를 요청한다.
- signaling server 를 사용해서 상대에게 offer 를 전송한다.
- 수신자는 offer 를 받고 PRCPeerConnection.setRemoteDescription()을 불러 remote description 에 저장한다.
- 응답자는 통신 종단을 위해 필요한 셋업을 수행한다. local media를 발견하고 해당 media track들을 TRCPeerConnection.addTrack()를 사용해 peer connection 에 저장한다.
- 응답자는 그러면 RTCPeerConnection.createAnswer()을 호출함으로 answer을 만든다.
- 마찬가지로 RTCPeerConnection.setLocalaDescription()을 호출해 local description 에 answer을 저장한다. 수신자는 이젲 양쪽 connection의 구성에 대해 안다.
- 수신자는 signaling server를 사용해서 송신자에게 answer를전달한다.
- 송신자는 answer을받음
- 송신자는 RTCPeerConnection.setRemoteDescription()을 사용해서 answer를 remote description 에 저장한다. 이제 양쪽 peer는 양쪽 구성에 대해 안다. media는 설정된데로 흐르기 시작한다
Pending and current descriptions
해당 process에 한스텝 더 깊이 들어가보자, 우리가 발견한 localDescription과 remoteDescription은 보이는것처럼 간단하지 않다. 왜나하면 재협상 과정에서 offer이 잘못된 포멧으로 인해 거부될수 있기 때문에 새로운 포멧을 생성하는 것은 중요하다. 하지만 실제로 다른 peer에서 받아들이기 전까지 실제로 전환하지 않아야 된다.이러한 이유로 webRTC는 pending 그리고 current dexcription을 사용한다.
- current description
current description(RTCPeerConnection.currentLocalDexcription, currentRemoteDescription에 의해 리턴된다.)현제 연결에서 실제로 사용되는 description을 보여준다. 이것은 양쪽에서 모두 사용하는것에 동의한 가장 최근의 연결이다.
- pending description
pending description(PTCPeerConnection.pendingLocalDescription, pendingRemoteDescription에 의해 리턴된다)은 setLocalDescription() 또는 setRemoteDescription()을 각각 호출한 후 현재 고려중인 description을 나타낸다.
description(PTCPeerConnection.localDescription, remoteDescription에 의해 return 된)을 읽을 때, 해당 description이 pending description 일때 pendingLocalDescription, pendingRemoteDescription의 값은 해당 description 의 return value 와 같다. (이것은 pending description 은 null이 될수 없다는걸 의미한다); 다시말해 current description(currentLocalDescription/ currentRemoteDescription)이 return 된다.
setLocalDescription() 또는 setRemoteDescription()을 호출함으로 descriptionㅇ르 변경할때 명시된 description은 pending description에 위치하게 된다. 그리고 WebRTC layer 에서 해당 description 이 적합한지 아닌지를 평가하기 시작한다. 일단 제안된 description이 통과되면 currentLocalDescription 또는 currentRemoteDescription은 pending description값으로 변경된다. 그리고 pending description은 null이 되고 pending description이 없다는것을 의미하게 된다,
<aside>
💡 NOTE: pendingLocalDescription은 고려중인 offer과 answer 뿐만 아니라, offer와 answer이 만들어지는동안 모여진 ICE 후보들도 포함한다. 비슷하게 pendingRemoteDescriptoin은 PTCPeerConnection.addIceCandidate()를 호출해 제공되어진 remote ICE cnadidate들도 포함한다
</aside>
ICE candidates
peer들은 media(압에서 논의한 offer/answer, SDP)에 대한 정보뿐만 아니라 네트워크 연결에 대한 정보도 교환해야 된다. 이것은 ICE 후보라고 알려져 있고, peer가 통신(직접적으로 또는 TURN server를 통해)하기 위한 가능한 방법들을 설명한다. 먼저 가장 좋은 후보들을 제안하고, 순차적으로 덜 좋은 후보들을 만든다. 이상적으로, 후보들은 UDP(이것은 빠르고 방해에 대해 상대적으로 쉽게 회복할 수 있다.)이다, 하지만 ICE기준은 TCP 후보들도 허용한다.
<aside>
💡 NOTE : 일반적으로 TCP를 사용하는 ICE후보들은 UDP가 사용불가능하거나 streaming 에 적합하지 않게 제안 될 때 사용할 수 있다. 하지만 모든 브라우저가 TCP ICE를 지원하는건 아니다.
</aside>
ICE는 TCP, UDP 연결을 여용하며, 일반적으로 UDP가 선호된다(그리고 더 지원이 잘된다). 각각의 protocol 은 몇가지 타입의 후보를 제공하고 각 후보타입들은 어떻게 peer to peer 에서 데이터가 전송되는지 정의한다.
- UDP candidate types
UDP 후보들(protocol이 udp로 설정된 후보)은 아래들중 한개의 type이 될수 있다
host
호스트 후보는 그 ip주소가 실제로 remote peer의 direct ip address 인 후보이다.
prflx
srflx
relay
- TCP candidate tytpes
active
passive
so
Choosing a candidate pair
ICE layer는 두개의 peer중 한개를 controlling agent 로 선택한다. ICE agent는 connection을 위해 어떤 candidate pair을 선택할지 최종 결정한다. 다른 peer는 controlled agent 라고 불린다. 너는 어느것이 너의 연결의 end point 인지 PTCIceCandidate.transport.role의 값을 확인함으로 확인할수있다. 이록 어느것이 어느것인지는 중요하지 않지만
controlling agent 는 어느 candidate pair를 사용할지 최종 결정하는 책임만 있는 것이 아니라, 필요할 경우, STUN server와 업데이트된 offer을 사용함으로 controlled agent에게 해당 선택을 시그널링한다.(어떤 candidate pair을 사용했는지 알려준다는 의미같은데?). controlled agent 는 단지 어떤 candidate pair 가 사용되었는지 말해주기를 기다린다.
하나의 ICE session에서 controlling agent 가 두개이상의 candidate pair를 선택할수 있다는 결과를 명심하는것은 중요하다. 매번 conrolling agent 가 candidate pair를 선택하고 해당 정보를 controlled agent 롸 공유할때마다 두 peer는 새로운 candidate pair로 인해 만들어진 새로운 configuration을 사용하기 위해 그들의 연결을 재설정한다
일단 ICE session이 완료되면, ICE reset이 발생하지 않는한 현재 적용중인 configuration는 최종 구성이다.
각각의 candidate 생성의 마지막에 end-of-candidates 알림이 candidate 속성이 빈 string 인 RTCIceCandidate의 형태로 전송된다.(ICE 후보들을 다 보냈고 빈 string 으로 인해 더 이상 보낼게 없다고 알릶) 해당 후보들은 addIceCandidate() method를 통해 connection에 추가될수 있고, remote peer에게 해당 알림을 보내기 위해서?
협상교환 과정에서 기대되어지는 후보군이 더이상 없을때 end-of-candidate 알람은 candidate 속성이 null인 RTCIceCandidate를 전달함으로 보내진다. 해당 메시지는 remote peer 에 전달될 필요는 없다. 구식의 알람임.
When things go wrong
협상 단계중에서,정상적으로 작동하지 않는 시간이 있다. 예를들어, 연결의 재협상중(하드웨어 또는 네트워크 상태 변경을 적용하기 위해서)에 협상이 교착상태에 빠지거나, 협상을 방해하는 에러가 발생할수 있다. 여기에는 ㄷ권한이슈나 또는 중요한 다른 문제들이 있을 수 도 있다.
ICE rollback
이미 활성중인 연결을 재협상하고 협상 실패하는 상황이 일어날때, 너는 이미 진행중인 연결을 죽이고 싶지 않을거다. 결국 너는 연결을 upgrade or downgrade하려고 시도했거나 session을 조정했을 것이다. 해당 상황에서 연결을 중단하는거는 과도한 행동일수 있다.
대신에 너는 ICE rollback을 시작할수 있다. rollback 은 SDP offer를 마지막연결의 signalingState가 stable이였던 configuration에 저장한다.
rollback을 시작하기 위해서 타입이 rollback인 description을 보낸다. 해당 description의 다른 모든 속성들은 무시된다
추가적으로, ICE agent은 offer를 생성한 peer가 다른 peer로부터 offer를 받을때 자동적으로 rollback이 실행된다. 다시 말해, local peer의 상태가 이전에 offer을 보낸적이 있는 have-local-offer
상태이고, remote peer로부터 수신받은 offer에 대해 setRemoteDescription()을 호출하면 remote peer가 호출자에서 local peer가 호출자로 변경된다
즉 이미 진행중인 연결을 중단하지 않고도 전환을 가능케 한다. 불필요한 연결 중단을 방지.
Establishing a connection: The WebRTC perfect negotiation pattern
해당 강과는 WebRTC의 완벽한 협상에 대해 소개합니다. 어떻게 작동하고, 왜 peer 사이의 webRtc connection을 협상하기 위해 추천되는 방식인지 그리고 해당 기술을 입증하기 위한 코드를 제공한다.
WebRtcsms 새로운 peer 연결을 하는 동안 signaling을 위한 특정 전송 메커니즘을 강제하지 않기 때문에 WebRTC는 매우 유연하다. 그러나 signaling 메시지의 전송, 통신에서의 유연성에도 불구하고 가능하면 따라야되는 디자인 패텅이 있고 완벽한 협상으로 알려져 있다.
WebRTC가 포함된 브라우저의 첫번째 배포 이후, 협상과정의 일부가 일반적으로 사용되는 경우보다 더 복잡하다는것을 깨달았다. 이것을 api의 몇몇 문제들과 방지해야될 잠재적인 경쟁상태 때문이다. 이러한 이슈들은 해결되었고, WebRTC의 협상을 상당히 단순화 할 수있었다. 완벽한 협상 패턴은 WebRTC의 초기 단계부터 협상이 개선된 방법의 예시이다.
Perfect negotiation concepts
완벽한 협상은 너의 어플리케이션 다른 로직과 완벽하고 원활하게 분리할수 있게 만든다. 협상은 본직적으로 비대칭적인 작업이다. 한쪽은 caller,”호출자”역할을 하고 다른 peer은 callee”피신자”역할을 한다. 완벽한 협상패던은 이러한 차이를 독립적인 협상 로직으로 분리하여 차이를 완화시킨다, 그래서 어플리케이션은 연결의 끝을 걱정할 필요가 없다. 너의 어플리케이션 관점으로 보면 전화를 받는거나 거는거나 차이가 없다.
perfect negotiation에 대한 가장좋은것은 caller와 callee에서 같은 코드를 사용하는 거다, 그래서 중복되는 협상코드를 작성할 필요가 없다.
perfect negotiation은 두 peer에 WebRTC connection 상태와 완전히 분리된 협상 과정에서 역할을 할당함으로 작동한다.
- polite(정중한 peer)
polite peer는 접근하는 offer에 대한 충돌을 방지하기 위해 rollback을 사용한다. 본질적으로 polite peer는 offer를 보낼수 있지만 다른 peer로부터 offer 가 도착한다면, “내 offer를 버리고 대신에 너의 offer를 고려한다” 라고 응답한다.
- impolite
상대의 offer를 무시한다. impolite는 polite peer에게 절대 사과하거나 포기하지않는다. 충돌이 발생하면 impolite peer가 이긴다.
이러한 경우에 양쪽 peer는 보낸 offer에 대한 충돌이 발생했을때 무엇을 해야하는지 정확히 안다. error conditions에 대한 대처가 더 예측 가능하다.
어떤 peer가 polite고 어떤 peer가 impolite인지 결정하는건 일반적으로 너에게 달려있다. 첫번째로 signaling server에 연결하는 per에 polite를 할당하는것처럼 간단하게 할 수도 있고 아니면 랜덤숫자를 부여해 승자를 polite로 할당하는 조금더 복잡한 방식으로 할 수도 있다. 너가 해당 결정을 하면 이러한 역할들은 두 peer에게 할당된고, 그들은 교착이 없고 관리를 위한 추가적인 코드가 필요없는 방법으로 함께 signaling을 관리 할수 있다.
고려해야 되는 중요한것이 있ㄷ다. caller와 callee의 역할들은 perfect negotiation중에 교환될수 있다(polite 와 impolite가 서로 뒤바뀔 수 있다). polite peer는 caller다, 이들은 offer를 보내지만 impolite peer로부터 충돌이 발생하면 polite peer는 그들의 offer를 버린다, 그리고 대신에 impolite peer 로부터 받은 offer에 답장을 한다(answer?). 이렇게 polite peer 는 caller 에서 callee 로 전환된다.
Implementing perfect negotiation
다음 perfect negotiation 패턴을 구현한 예제를 보자. 해당 코드에서 signaling server 와 통신하기 위해 사용되는 SignalingChannel
이 있다고 가정하자. 너의 코드에서 당연히 너가 좋아하는 signaling 기술을 사용할수 있다.
이 코드는 양쪽 peer에 상관없이 동일하다
Create the signaling and peer connections
먼저 signaling channel 은 열려있어야 되고 RTCPeerConnection
은 생성되있어야 된다. 여기서 사용되는 STUN srever는 명백하게 진짜가 아니다; 너가 사용하려면 진짜 STUN server로 교환해야된다.
const config = {
iceServers: [{ urls: "stun:stun.mystunserver.tld" }],
};
const signaler = new SignalingChannel();
const pc = new RTCPeerConnection(config);
해당 코드는 또한 “selfview”, “remoteview”를 class로 가지고 있는 <video>
element가 필요하다. 이것들은 local user의 self view와 remote peer 로부더 들어오는 stream view 를 포함한다.
Connecting to a remote peer
const constraints = { audio: true, video: true };
const selfVideo = document.querySelector("video.selfview");
const remoteVideo = document.querySelector("video.remoteview");
async function start() {
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
for (const track of stream.getTracks()) {
pc.addTrack(track, stream);
}
selfVideo.srcObject = stream;
} catch (err) {
console.error(err);
}
}
위에 보여지는 start()
함수는 다른 peer 와 연결을 원하는 양쪽 어디서든지 호출이 가능하다. 누가 먼저 수행하는지는 중요하지 않다. 협상은 잘 이루어진다. 이것은 이전의 WebRTC 연결 코드와 크게 다르지 않습니다. user의 카메라와, 마이크는 getUserMedia()
를 통해 획득 될 수 있다. media track의 결과는 addTrack()
을 통과시킴으로 RTCPeerConnection
에 추가된다. 그러면 마지막으로 selfVideo class를 가리키는 <video>
elements를 위한 media source에는 camera와 마이크의 stream 이 설정되고, 다른쪽 peer가 보는것을 local peer가 볼수 있다.
Handling incoming tracks
우리는 peer connection에서 받아들이기로 협상되어 있는 video와 audio track을 다루기 위한 track event위해 handler를 설정해야된다. 이것을 하기 위해서 우리는 RTCPeerConnection
의 ontrack
event handler를 만들어야 된다
pc.ontrack = ({ track, streams }) => {
track.onunmute = () => {
if (remoteVideo.srcObject) {
return;
}
remoteVideo.srcObject = streams[0];
};
};
track
event 가 발생하면, 이 handler는 실행된다. destructuring
을 사용하여 RTCTrackEvent
의 track
과 streams
속성을 추출할 수 있다. track(former)은 수신중인 video track이거나 audio track이다. streams(latter)는 MediaStream
의 배열로 이 track을 포함하는 stream을 나타낸다(드물게 한 track이 여러게의 stream에 속할 수도 있다). 우리의 경우, 앞서 ㅁddTrack을 통해 한개의 stream만 통과시켰기 때문에 오직 index가 0인 한개의 stream만 포함한다.
track이 packets을 수신하기 시작하면 unmuted되기 때문에 unmute event handler를 추가했다. 그리고 우리의 나머지 코드를 여기에 넣었다.
우리가 이미 remote peer로부터 video 가 들어오고 있으면, 우리는 아무일도 하지 않는다.(코드에서 if를 통해 srcObject
값이 있는지 체크를 통해 구현). 그리고 우리는 srcObject
에 stream배열의 index 0번째 값을 넣는다.
The Perfect negotiation logic
이제 우리는 어플리케이션의 나머지 부분과 완정히 독립된 perfect negotiation logic 으로 들어간다
Handling the negotiationneeded event
먼저, 우리는 local description 을 획득하고 signaling channerl을 사용해 remote peer에게 보내기 위해 RTCPeerrConnection 이벤트 헨들러인 onnegotiationneeded를 만들어야 된다.
let makingOffer = false;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
} catch (err) {
console.error(err);
} finally {
makingOffer = false;
}
};
인자가 없는 setLocalDescription()은 현재의 signalingState
에 따라 적합한 description을 생성하고 세팅한다. 세팅된 description은 가장 가장 최근에 remote peer로부터의 offer에 대한 answer이거나, 협상이 진행중이 아닐때 새로 생성된 offer이다. 협상 event는 안정된 상태에서만 발생하기 때문에 offer는 항상 생성된다?
우리는 Boolean 타입의 변수, makingOffer
을 true
로 설정해 우리가 offer를 준비중이라고 표시한다. 경쟁을 피하기 위해서, 우리는 offer가 처리중인지 아닌지를 결정하기 위해 signaling state 대신에 해당 makingOffer을 나중에 사용할거다. signalingState
는 비동기적이기 때문에 충돌 가능성을 가진다.
일단 offer가 생성이 되면, 세팅하고 전송한다(아니면 error 가 발생하거나). 그리고 makingOffer
false로 되돌아 간다
Handling incoming ICE candidates
다음, 우리는 RTCPeerConnection
의 icecandidate
이벤트를 처리해야된다. 이 이벤트는 loca UCE layer가 어떻게 signaling channel을 통과해 remote peer에게 전달할 후보들을 우리에게 전달하는지에 대한 이벤트이다.
pc.onicecandidate = ({ candidate }) => signaler.send({ candidate });
위의 코드는 ICE event의 candidate member를 얻고, signaling channel의 send()
method를 사용해 signaling server 너머의 remote peer 로 전달된다.
Handling incoming messages on the signaling channel
signaling server로부터 들어오는 메시지를 다루기 위한 코드는 이 퍼즐의 마지막 조각이다. 해당 과정은 signaling channel object에서 onmessage
이벤트 헨들러를 통해 구현된다. 해당 method는 signaling server로부터 message가 도착할때마다 호출 된다.
let ignoreOffer = false;
signaler.onmessage = async ({ data: { description, candidate } }) => {
try {
if (description) {
const offerCollision =
description.type === "offer" &&
(makingOffer || pc.signalingState !== "stable");
ignoreOffer = !polite && offerCollision;
if (ignoreOffer) {
return;
}
await pc.setRemoteDescription(description);
if (description.type === "offer") {
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
}
} else if (candidate) {
try {
await pc.addIceCandidate(candidate);
} catch (err) {
if (!ignoreOffer) {
throw err;
}
}
}
} catch (err) {
console.error(err);
}
};
SignalingChannel
로부터 onmessage
event handler 를 통해 들어온 message를 수신할때, 수신된 JSON object는 description
과 candidate
로 분리된다. 만약 들어온 message가 description
이면, 다른 peer가 보낸 offer 또는 answer이다.
다른 경우에 message가 candidate
이면, trikle ICE
의 일환으로 remote peer 로부터 받은 ICE 후보들이다. 해당 후보들은 addIceCandidate()
를 통과함으로 local ICE layer에 전달되어야 된다.
On Receiving A Description
우리가 description을 수신한다면, 우리는 들어오는 offer 또는 answer에 대해 응답할 준비를 해야된다. 먼저 우리는 offer 를 받아들일수 있는 상태인지 확인한다. 만약 connection’s signaling state가 불안정하거나, 우리 연결의 end point가 우리의 offer를 만드는 작업을 실행했다면, 우리는 offer 충돌을 조심해야된다.
만약 우리가 impolite peer 이고, 충돌된 offer를 수신했다면, 우리는 setting 이 되지 않은 description 을 응답하고, ignoreOffer
을 true로 세팅해 해당 offer에 속하는 signaling channel로부터 보내진 모든 후보들을 무시한다. 이렇게 함으로 우리는 해당 offer에 대해서 우리에게 알리지 않았기 때문에 error를 피할수 있다.
만약 우리가 polite peer이고 똑같이 충돌된 offer를 받았으면, 우리는 특별한게 필요하지 않다, 우리의 offer는 다음 과정에서 자동으로 rolled back되기 때문이다.
우리가 offer를 받고싶은것을 확인한 후, 우리는 setRemoteDescription()
을 호출해 수신받은 offer를remote description에 세팅한다. 이것은 다른 peer의 configuration을 WebRTC가 알게 한다. 만약 우리가 polite peer이면, 우리 offer를 버리고 새로운것을 받아들인다?
새로운 remote description이 offer라면, 우리는 WebRTC에게 RTCPeerConnection
의 setLocalDescription()
메소드를 호출함으로 적절한 local configuration을 선택하게 한다. 이 경우에는 setLocalDescription()
이 수신된 offer에 대한 적절한 answer를 자동으로 만드는것을 유발한다. 그러면 우리는 signaling channel을 통해 answer를 첫번째 peer 에게 전송한다.
On Receiving An ICE Candidate
다른 경우에, 수신된 메시지가 ICE 후보를 들고있으면, 우리는 addIceCandidate()
를 호출함으로 local ICE layer 에 해당 후보를 전달한다. 만약 에러가 발생하고, 우리가 가장 최근의 offer를 무시했다면, 우리는 후보를 추가하려고 시도할때 발생하는 모든에러 또한 무시한다.
Making negotiation perfect
너가 perfect negotiation 을 완벽하게 만드는 것에 대해 궁금하다면, 해당 챕터는 너를 위한것이다. 여기, 우리는 perfect negotiation 을 가능하게 만들기위해 WebRTC API에 가해진 변경과 모범사례를 살펴볼거다.
Glare-free setLocalDescription() : 충돌으로 부터 자유로운 setLocalDescription()
과거에, negotiationneeded
이벤트는 일찍이부터 충돌에 취약한 방식으로 다루었다. 양쪽의 peer가 동시간에 offer를 만드는것을 시도하고, 그로인해 한쪽 peer에서 error 가 발생하고 연결 시도를 포기하는. 경우가 있었다.
The old way
아래의 onnegotiationneeded
이벤트 핸들러에 대해 생각해보자
pc.onnegotiationneeded = async () => {
try {
await pc.setLocalDescription(await pc.createOffer());
signaler.send({ description: pc.localDescription });
} catch (err) {
console.error(err);
}
};
createOffer()
method가 비동기적이고 처리를 완료하는데 약간의 시간이 걸리고 해당 시간동안 remote peer에서 offer를 생성하고 보낼수 있기 때문에, 우리에게 안정된 상태를 벗어나게 만들고 have-remote-offer
상태에 들어가게 된다. 해당 상태는 우리가 offer에 대한 응답을 기다리는 상태이다. 하지만 remote peer에서 우리가 보낸 offer를 받으면 remote peer도 같은 상태가 된다. 이로인해 양쪽 peer 모두 connection을 완료할 수 없는 상태가 된다.
Perferct negotiation with the updated API
해당 Implementing perfect negotiation 챕터에서 본것처럼, 우리는 현재 우리가 offer를 보내는 처리과정임을 가리키는 변수(여기서는 makingOffer
라고 불린다)를 추가하고 업데이트된 setLocalDescription()
method를 사용함으로 해당 문제를 제거할 수있다.
let makingOffer = false;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
} catch (err) {
console.error(err);
} finally {
makingOffer = false;
}
};
우리는 setLocalDescription()
을 호출하기 전에 offer를 전달하는것을 방해하는것에 대하여 대응하기 위해서 makingOffer
을 세팅 할 수있다. 그리고 해당 offer가 signaling server에 전달될때까지(또는 offer 생성을 막는 에러가 발생할 때까지) false
로 값을 되돌릴수 없다. 이 경우에, 우리는 offer충돌 리스크를 피할 수 있다.
Automatic rollback in setRemoteDescription()
perfect negotiation을 위한 key point 는 polite peer의 개념이다. 해당 개념은 offer에 대한 answer을 기다리고 있는도중 offer를 받았을때 항상 스스로 roll back 하는것이다. 이전에는 rollback를 실행하기 위해서 rollback 조건을 수동으로 체크하고, rollback을 수동으로 실행시켜야 됬다. localDescription의 type을 rollback
으로 설정함으로, 다음과 같이
await pc.setLocalDescription({ type: "rollback" });
이렇게 함으로 이전의 상태가 무엇이였던지 상관없이 local peer 는 안정적인 signalingState
로 되돌아가게 된다. peer는 stable
한 상태에서만 offer를 받아들일수 있기 때문에, peer는 자신의 offer는 폐지하고, remote(impolite) peer로부터 offer를 받아들일 준비를 한다. 그러나 우리가 나중에 보겠지만, 이러한 접근에는 문제가 있다.
Perfect negotiation with the old API
이전의 API를 사용하여 perfect negotiation 도중에 negotiation message를 수신하는것을 구현하는것은 다름과 같아보일수 있다
signaler.onmessage = async ({ data: { description, candidate } }) => {
try {
if (description) {
if (description.type === "offer" && pc.signalingState !== "stable") {
if (!polite) {
return;
}
await Promise.all([
pc.setLocalDescription({ type: "rollback" }),
pc.setRemoteDescription(description),
]);
} else {
await pc.setRemoteDescription(description);
}
if (description.type === "offer") {
await pc.setLocalDescription(await pc.createAnswer());
signaler.send({ description: pc.localDescription });
}
} else if (candidate) {
try {
await pc.addIceCandidate(candidate);
} catch (err) {
if (!ignoreOffer) {
throw err;
}
}
}
} catch (err) {
console.error(err);
}
};
rollback은 다음 협상때(다음 협상은 현재 한협상이 끝난후에 즉시 시작된다)까지 변경을 연기하는 일을하기 때문에 polite peer는 이미 보낸 offer에 대해 기다리는 동안 수신된 offer를 언제 버려야 할지 알아야된다.
코드에서 message가 offer인지 체크하고, offer이면 local signaling state가 unstable
인지 체크한다. 만약에 불안전한상태이고 local peer가 polite이면, 우리는 rollback을 실행시켜야된다. 그래서 우리는 나가는 offer를 들어오는 offer로 교체하기 위해 rolll-back을 실행한다. 그리고 이것들은 수신된 offer를 처리하기전에 완료되어야 된다.
여기에는 “roll back하고 수신된 offer를 대신사용한다” 라는 단일 작업이 없기 때문에, polite peer에서 해당 변화를 수행하는데 Promise.all()
내부에서 수행되는 두단계가 필요하다. 수신받은 offer가 처리되기 전에 두작업이 완료된것을 확신해야된다. 먼저 rollback을 수행하고 그 다음에 수신받은 offer를 remote dscription에 세팅한다. 이는 polite에서 보낸 offer를 localDescription에서 지우고 remote peer에서 보낸 offer를 remoteDescription으로 저장하는 작업을 완료한다. 그래서 처음에 offer를 보내서 caller였던 polite peer는 이제 offer을 받고 answer을 응답하는 callee가 되었다.
impolite peer로 부터 수신받은 모든 다른 dscription들은 setRemoteDescription()
을 통과함으로 일반적으로 처리된다.
마지막으로, 우리는 createAnswer()
에 의해 return 된 answer을 우리 local description에 세팅하기 위해서 setLocalDesciption()
을 호출함으로 수신받은 offer를 처리할 수 있다. 그리고 나서 polite peer에게 signaling channel 를 사용해서 description 을 보낼 수 있다? ← 뭔소리임??
만약 수신된 message가 SDP description이 아니라 ICE candidate이면 PTCPeerConnection
의 addIceCandidate()
를 통과해 ICE later에게 전달된다. 만약 여기서 에러가 발생하거나 impolite peer 가 충돌중이기 때문에 해당 offer를 버리지 못하면, 우리는 error 를 발생시켜 caller 가 해당 에러를 처리할수 있게 한다. 그렇지 않으면, 우리는 해당 오류를 무시하고 버린다. 이 맥락에서는 중요하지 않기 때문에??
Perfect negotiation with the updated API
개정된 코드는 우리에게 매개변수없이 setLocalDescription()
을 할수있다는 점과 게다가 필요한 경우 setRemoteDescription()
이 자동적으로 roll back한다는 이점이 있다. 이것은 우리가 시간을 맞추기 위해서 Promise
를 사용해야 될 필요를 제거했다. roll back이 setRemoteDescription()
호출의 본질적으로 원초적인 부분이 되었기 때문이다.
let ignoreOffer = false;
signaler.onmessage = async ({ data: { description, candidate } }) => {
try {
if (description) {
const offerCollision =
description.type === "offer" &&
(makingOffer || pc.signalingState !== "stable");
ignoreOffer = !polite && offerCollision;
//impolite peer 이면 return 되서 종료
if (ignoreOffer) {
return;
}
//polite peer 이면 offerIgnore하고 계속 진행
await pc.setRemoteDescription(description);
if (description.type === "offer") {
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
}
} else if (candidate) {
try {
await pc.addIceCandidate(candidate);
} catch (err) {
if (!ignoreOffer) {
throw err;
}
}
}
} catch (err) {
console.error(err);
}
};
코드의 길이차이는 미미하고 복잡도도 크게 줄기는 않았지만 해당 코드는 훨신 신뢰 할 수있다. 이제 어떻게 해당 코드가 작동하는지 알아보자.
On Receiving A Description
개정된 코드에서, 만약 수신된 메시지가 SDP description
이면, 우리는 우리가 offer를 전달을 시도중인지 확인한다. 만약 수신된 메시지가 offer
이고 local peer가 impolite peer이고, 충돌이 발생하면, 우리는 해당 offer를 무시한다. 왜냐하면 우리는 우리가 이미 보낸 offer가 계속해서 사용되길 원하기 때문이다. 이것이 impolite peer의 행동이다.
다른 경우에, 우리는 대신 수신된 message를 처리하려고 시도한다. 이것은 수신된 description
을 setRemoteDescription()
을 통과함으로 remote description에 세팅함으로 시작된다. 해당일은 offer인지 answer인지 관계없이 작동한다. 필요할 경우 rollback이 작동한다.
여기서, 수신된 메시지가 offer
이면, 우리는 setLocalDescription()
을사용해서 적절한 local description을 생성하고 세팅한다. 그리고 signaling server를 사용해서 remote peer에게 description을 보낸다. perfect negotiation의 개선은 RTCPeerConnection
에서의 restartIce()
method를 추가함으로 고처졌다.
Explicit restartIce() method aded
이전 기술에서 negotiationeeded
event를 사용하는 동안 ICE restart
를 발생시키는 것은 명백한 결함이 있다. 이 결함들은 negotiation을 진행하는 동안 안정적이고, 신뢰성있게 작동하는것을 어렵게 만들었다.
The old way
과거에, 너가 ICE 에러를 직면하고, negotiation 재시작이 필요할 때, 너는 아마 이렇게 했을거다
pc.onnegotiationneeded = async (options) => {
await pc.setLocalDescription(await pc.createOffer(options));
signaler.send({ description: pc.localDescription });
};
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === "failed") {
pc.onnegotiationneeded({ iceRestart: true });
}
};
이것은 몇가지 신뢰성 이슈와, 버그를 가진다(signaling state가 stable
이 아닐때 iceconnectionstatechange
event 를 시작하면 실패하는거와 같은), 하지만 iceRestart
option을 true
로 offer를 생성하고, 보내지 않는한 ICE restart를 요청할 방법은 없다. restart request 요청을 보내기 위해서는 negotiationneeded
event handler를 직접호출해야된다. 해당 과정을 제대로 수행하기 위해서는 최선의 경우에도 까다롭도, 실수하기 쉬워서 버그가 일반적으로 흔했다.
Using restartIce()
이제, 너는 restartIce()
를 사용해 더 깔끔해게 작업할 수 있다.
let makingOffer = false;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
} catch (err) {
console.error(err);
} finally {
makingOffer = false;
}
};
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === "failed") {
pc.restartIce();
}
};
ICE restart를 실행하기 위해 opotions을 가진 onnegotiationneeded
를 직접 호출하는 대신에 이 개선된 기술은 ICE connection state
가 failed
로 restartIce()
를 호출한다. restartIce()
는 ICE layer에게 다음 ICE message를 보낼때부터 iceResatrt
를 자동적으로 추가하라고 말한다. 문제 해결!
Rollback no longer supperted in the pranswer state
API 변경사항에서 주목해야될것은 더이상 have-remote-pranswer
또는 have-local-pranswer
상태일때 roll back을 하지 못한다. 다행스럽게도, perfecto negotiation이 사용될때 해당 작업을 할 필요가 없다. 이러한 상황은 rollback 이 필요해지기 전에 발경되고 방지되기 때문이다.
그러므로, 두 상태를 가질때 rollback을 시도하는것은 InvalidStateError
를 발생시킨다.
Lifetime of a WebRTC session
Signaling and video calling
RTCPeerConnection(docs 주소 ← 한번 읽어보기)
RTCPeerConnection 인터페이스는 local peer 와 remote peer 사이의 webrtc 연결을 나타낸다. 이것은 remote peer와 연결하고 연결을 유지, 관리하고 더이상 필요하지 않은 연결을 종료하는 method를 제공한다.