실시간 영상 채팅 Web RTC

bow Rain·2021년 11월 13일
0

개발 관련 정리

목록 보기
10/19
post-thumbnail

webRTC는 Web Real Time Communication의 약자로 플러그인 필요없이 클라이언트와 클라이언트간 P2P 영상/음성/데이터 통신 가능한 오픈소스 표준이며 구글에서 처음 공개되서 Mozilla, Opera, MS 까지 기술적 표준을 만들고있다.

WebRTC 는 P2P 형식으로 통신이 이루어지고
peer간의 실제 통신은 Signalling이라는 메타데이터 교환으로 이루어짐
peer간의 통신을 위해서 중계 서버가 필요함 이걸 Signalling Server라고한다.

대표 API 3가지 :
MediaStream (getUserMedia)
RTCPeerConnection
RTCDataChannel

ICE : Interactive Connectivity Establishment의 약자로 두 단말이 서로 통신할수 있는 최적의 경로를 찾을수있도록
도와주는 프레임워크이다.

STUN : Session Traversal Utilities for NAT의
약자로 자신의 공인 아이피를 알아오기위해 STUN 서버에 요청하고 STUN 서버는 공인 IP주소를 응답한다.

TRUN : Traversal Using Relays around NAT 의 약자 NAT 또는 방화벽에서 보조하는 프로토콜. 클라이언트는 직접 서버와 통신 하지않고 TURN 서버를 경유한다.

WebRTC

Fetching : 상대 peer에게 보낼 음성 및 영상 데이터를 수집
Signaling : 상대 peer와 연결을 맺기 위해 상대 peer를 탐색
Connection : 발견한 대상과 peer to peer Connection을 맺음 Channel을 개방함
Communication : 개방해놓은 채널을 통해 통신을 주고받음

1,Fetching 단계

MediaStream ,getUserMedia를 통해 영상 및 음성 정보를 가져옴

2,Signaling 단계

1, 네트워크 정보 교환
ICE Framework를 이용해서 find candidate
즉 ip와 port를 탐색함

2, Media Capability 정보를 교환
sdp(Session Description Protocol) 형식을 따르는 blob인 offer와 answer를 주고 받으며 교환함

3, Session Control Message를 교환함

네트워크 정보 교환 단계

peerConnection.onicecandidate : 핸들러를 통해 현재 내 클라이언트의 ICE Candidate(네트워크 정보)가 확보되면 실행될 Callback을 전달함.

ICE Candidate(네트워크 정보)가 확보되면 중간 매개자인 Signaling Server를 통해
상대 peer에게 serialized된 ice candidate 정보를 전송함 (서로 교환)

서로 네트워크 정보가 도착하면 RTCPeerConnection.addIceCandidate를 통해 서로 네트워크 정보를 등록함

Media Capability 정보를 교환

상대방이 RTCPeerConnection.createOffer를 호출해서 Offer SDP (Session Description Protocol) 을 생성

상대가 Offer SDP를 Signaling Server를 통해 전송함

나는 Signaling Channel에서 Offer SDP를 받아서 RTCPeerConnection.setRemoteDescriptio를 수행

상대의 session정보를 알아왔으니 RTCPeerConnection.createAnswer를 호출하여서 Answer SDP를 생성후 SignalingChannel을통해 상대에게 전달함.

setRemoteDescription 이 성공적으로 수행되면 서로 peer를 알고 p2p연결에 성공한 상태이다.

3,Connection 단계

RTCPeerConnection 을 얻은 단계 성공적으로 연결됨

4,Communication 단계

video 나 audio 데이터 스트림 의 경우

전송 : 자신의 머신에서 getUserMedia 등 video/audio 스트림을 획득
RTCPeerConnection 을 생성할 당시에 addTrack(데이터 stream 채널을 연결) 해줌. Signaling 을 통해 connection이 이루어지기 전에 미리 되어야함.

수신 : RTCPeerConnection.ontrack 의 callback 을 커스텀하게 설정함 connection 이 성공적으로 이루어진 후에 상대방의 Track (video/audio stream) 이 감지되면 어떤 동작을 할지 설정할 수 있음 보통 받은 track 의 데이터 스트림을 DOM 의 element 에 연결해 보여줌.

<video srcObject={??}/>

직렬화된 text 데이터 의 경우

전송 : RTCPeerConnection.createDataChannel 을 통해 특정 이름의 data 전달 채널을 개설할 수 있음. Signaling 을 통해 connection 이 이루어지기 전에 미리 되어야함.

수신 : RTCPeerConnection.ondatachannel 의 callback 을 커스텀하게 설정함 connection 이 성공적으로 이루어진 후에 상대방이 data channel 을 통해 어떤 데이터를 보냈을 때의 동작을 설정할 수 있음.

	//---------------------------- 상담 마이크 비디오 설정 -------------------------------------
	// ============================
	// 필요한 변수 등록
	// ============================
	const video1 = document.getElementById('video1');
	const video2 = document.getElementById('video2');
	let flag = true;
	let pc;
	let localStream;
	let remoteStream;
	let rtc_peer_connection = null;
	let rtc_session_description = null;
	let get_user_media = null;
	let user_usid;
	let counselText="";
	// ============================
	// TURN & STUN 서버 등록
	// ============================
	const configuration = {
		'iceServers' : [ {
			'urls' : 'stun:stun.l.google.com:19302'
		}, {
			'url' : 'turn:numb.viagenie.ca',
			'credential' : 'muazkh',
			'username' : 'webrtc@live.com'
		} ]
	};
	
			//---------------------------- signaling 서버 -------------------------------------
			
			// ============================
			// 핸들러 위치
			// package com.kh.john.exboard.socket.ExpertHandler
			// ============================
				
			//const conn = new WebSocket('wss://rclass.iptime.org${path}/ertc');
			const conn = new WebSocket('wss://192.168.219.105${path}/ertc');
			conn.onopen = function() {
				//console.log("onopen => signaling server 연결");
				if ("${loginMember.memClass}" != '전문가') {
					sendMessage(new ExboardMsg("SYS",
							"${loginMember.memNickname}", "접속",
							"${loginMember.usid}"));
				}
			};
			// ============================
			// 받은 메세지를 분기 처리해서 나눔
			// ============================
			conn.onmessage = function(msg) {
				//console.log("onmessage => 메세지 출력 : " + msg);
				let content = JSON.parse(msg.data);
				//console.log("content.type : " + content.type);
				if (content.type === 'expert') {
					//console.log(" === 분기 expert === ");
					start();
				} else if (content.type === 'offer') {
					//console.log(" === 분기 offer === ");
					start();
					pc
							.setRemoteDescription(new rtc_session_description(
									content));
					doAnswer();
				} else if (content.type === 'answer') {
					//console.log(" === 분기 answer === ");
					pc
							.setRemoteDescription(new rtc_session_description(
									content));
				} else if (content.type === 'candidate') {
					//console.log(" === 분기 candidate === ");
					let candidate = new RTCIceCandidate({
						sdpMLineIndex : content.label,
						candidate : content.candidate
					});
					pc.addIceCandidate(candidate);
				} else if (content.type == 'SYS') {
					//console.log(" === 분기 SYS === ");
					start();
					user_usid = content.id;
				} else if (content.type == 'TXT') {
					//console.log(" === 분기 TXT === ");
					$("#edit").html(content.msg);
					$("#edit").scrollTop($("#edit")[0].scrollHeight);
				} else if (content.type == 'CAM') {
					//console.log(" === 분기 CAM === ");
					if (content.msg === 'off') {
						video2.srcObject = null;
					} else {
						video2.srcObject = remoteStream;
					}
				} else if (content.type == 'FILE2') {
					//console.log(" === 분기 FILE2 === ");
					//웹소켓 업로드 일반용 다음 구현한거
					//console.log("content : " + content.msg);
					imgDivPrint2(content.msg);
				} else if (content.type == 'END') {
					//console.log(" === 분기 END === ");
					location.replace('${path}/board/boardList');
				}else if(content.type == 'MEMEND'){
					//console.log(" === 분기 MEMEND === ");
					let experthtml = "";
					
					experthtml += "<div class='button-8'> <div class='eff-8'></div><a class='johnbtn' onclick='counselEnd();'>완료</a></div>";
					experthtml += "<div class='button-7'><div class='eff-7'></div><a class='johnbtn' onclick='memInfoView();'>정보</a></div>";
					$("#buttonDiv").html(experthtml);
				}
			};
			conn.onclose = function() {
				//console.log('onclose 실행');
			};
			function sendMessage(message) {
				conn.send(JSON.stringify(message));
				//console.log("메세지 보내는 함수 sendMessage");
			};
			//---------------------------- 비디오 적합 여부 설정 -------------------------------------
		// ============================
		// 비디오 오디오 설정, 막 설정하면 안되고 지원이 되는지 보고 높여야된다.
		// ============================
		const constraints = {
					  video: {width: {exact: 1280}, height: {exact: 720}},
				    audio : true
				}; 
			
		// ============================
		// 브라우저 적합성 확인
		// ============================
			if (navigator.getUserMedia) {
				//console.log("getUserMedia");
				get_user_media = navigator.getUserMedia.bind(navigator);
				//get_user_media = navigator.mediaDevices.getUserMedia(constraints);
				//console.log("navigator : "+navigator);
				//console.log("navigator.getUserMedia.bind(navigator) : "+navigator.getUserMedia.bind(navigator));
				videoStart();
				rtc_peer_connection = RTCPeerConnection;
				rtc_session_description = RTCSessionDescription;
			} else if (navigator.mozGetUserMedia) {
				//console.log("mozGetUserMedia");
				get_user_media = navigator.mozGetUserMedia.bind(navigator);
				videoStart();
				rtc_peer_connection = mozRTCPeerConnection;
				rtc_session_description = mozRTCSessionDescription;
			} else if (navigator.webkitGetUserMedia) {
				//console.log("webkitGetUserMedia");
				get_user_media = navigator.webkitGetUserMedia.bind(navigator);
				videoStart();
				rtc_peer_connection = webkitRTCPeerConnection;
				rtc_session_description = webkitRTCSessionDescription;
			} else {
				//console.log("지원안하는 브라우저");
				alert("지원하지 않는 브라우저입니다. firefox chrome브라우저를 이용하세요");
				flag = false;
			}
			// ============================
			// 오디오나 비디오 사용여부 확인 NOT FOUND가 뜨면 장치 설정에 문제가 있을수있다. 드라이버를 다시 받아보거나 삽질해보기
			// ============================
		 	function videoStart() {
				//console.log("constraints : "+constraints);
				//console.log("get_user_media : "+get_user_media);
				get_user_media(constraints, function(stream) {
					//console.log('stream 함수 => 스트림 요청 성공');
					localStream = stream;
					//console.log("localStream : " + localStream);
					if ("${loginMember.memClass}" == '전문가') {
						video1.srcObject = localStream;
					}
					sendMessage(new ExboardMsg("expert"));
					//console.log("메세지 보냄!");
					//console.log("gotStream 함수 => start 실행");
					start();
				}, function(e) {
					alert('카메라 오류 : ' + e+" \n 메세지 : "+e.message);
				});
			};
			
	
			//---------------------------- P2P 연결 로직 -------------------------------------
			// ============================
			// 상담이 시작될때 실행되는 메소드
			// ============================
			function start() {
				if (flag && typeof localStream !== 'undefined') {
					//console.log("peer 연결 부분 분기 진입");
					createPeerConnection();
					pc.addStream(localStream);
					flag = false;
					//console.log("do call 실행됨 ");
					doCall();
				}
			};
			// ============================
			// PeerConnection객체를 생성하고 Media Capability 정보를 교환 sdp(Session Description Protocol) 형식을 따르는 blob인 offer와 answer를 주고 받으며 교환함
			// ============================
			function createPeerConnection() {
				//console.log("createPeerConnection 실행");
				try {
					//configuration에는 STUN & TURN 서버가 있음
					//STUN : Session Traversal Utilities for NAT의 약자로 자신의 공인 아이피를 알아오기위해 STUN 서버에 요청하고 STUN 서버는 공인 IP주소를 응답함.
					//TURN : Traversal Using Relays around NAT 의 약자 NAT 또는 방화벽에서 보조하는 프로토콜. 클라이언트는 직접 서버와 통신 하지않고 TURN 서버를 경유함.
					pc = new rtc_peer_connection(configuration);
					pc.onicecandidate = handleIceCandidate;
					pc.onaddstream = handleRemoteStreamAdded;
					pc.onremovestream = handleRemoteStreamRemoved;
					//console.log(" RTCPeerConnection 생성 완료 ");
				} catch (e) {
					//console.log(" RTCPeerConnection 생성 에러발생 : " + e.message);
					alert("RTCPeerConnection 에러");
					return;
				}
			};
			// ============================
			// createPeerConnection 함수에서 호출함
			// ============================
			function handleRemoteStreamAdded(event) {
				//console.log("RemoteStream 추가됨");
				//원격 스트림에 스트림을 넣어줌
				remoteStream = event.stream;
				if ("${loginMember.memClass}" != '전문가') {
					video2.srcObject = remoteStream;
				}
				/* else{
					if(user_cam != false){
						video2.srcObject = remoteStream;
					} 
				}
				 */
			};
			// ============================
			// start 함수에서 호출함
			// ============================
			function doCall() {
				//console.log("createOff 함수를 통해서 통신 요청");
				pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
			};
			// ============================
			// 소켓 메세지 에서 분기 처리로 호출함
			// ============================
			function doAnswer() {
				//console.log('peer에게 응답 보내기.');
				pc.createAnswer().then(setLocalAndSendMessage,
						onCreateSessionDescriptionError);
			};
		
			// ============================
			//핸들러 후보 상대방 탐색
			//ICE : Interactive Connectivity Establishment의 약자로 두 단말이 서로 통신할수 있는 최적의 경로를 찾을수있도록 도와주는 프레임워크임.
			// ============================
			function handleIceCandidate(event) {
				//console.log('icecandidate 실행 event : ' + event);
				if (event.candidate) {
					//console.log('icecandidate 응답 보내기 ');
					sendMessage({
						type : 'candidate',
						label : event.candidate.sdpMLineIndex,
						id : event.candidate.sdpMid,
						candidate : event.candidate.candidate
					});
				} else {
					//console.log('handleIceCandidate 탐색 종료');
				}
			};
			function handleRemoteStreamRemoved(event) {
				//console.log('원격 스트림 삭제됨 Event : ' + event);
			};
			function setLocalAndSendMessage(sessionDescription) {
				pc.setLocalDescription(sessionDescription);
				//console.log("setLocalAndSendMessage 응답 보내기 : "
						+ sessionDescription);
				sendMessage(sessionDescription);
			};

	

WEB RTC 참고 링크

0개의 댓글