이전 글에 이어서 WebRTC 를 사용해서 실시간 화면 공유를 구현해보았습니다.
저번 글에 '화면 공유 기능 목표!!' 라고 이야기했던게 사실 오래 걸릴 줄 알았는데 의외로 금방? 끝났습니다ㅋㅋ
라고 이전에 글을 썼었는데...생각해보니 공유 되는 나의 화면을 나만 볼 수 있었고, 내 화면을 다른 사람이 보는 내가 생각했던 진짜 '화면 공유' 기능이 아니었다는 걸 깨달았습니다. 결국 빠르게 글을 잠금표시로 전환해두고 코드를 뜯어고치기 시작했습니다.
솔직히 금방 끝나서 다행이다! 했는데 결국 이렇게 또 한주가 가버렸네요ㅠ.ㅠ
JS 부분만 고치면 되는거여서 Java 부분의 코드 수정은 전혀 없었습니다
코드는 언제나 처럼 git 참고!!
https://github.com/SeJonJ/Spring-WebSocket-WebRTC-Chatting
0. MediaStream
화면 공유를 하기위해서는 WebRTC 에서 지원하는 기중 중 하나인 navigator.mediaDevices.getDisplayMedia 를 사용한다. 사실 이 부분은 이전 화상 채팅 코드에서 "유저의 비디오 카메라, 마이크 미디어"를 가져오기 위해서 navigator.mediaDevices.getUserMedia() 를 사용했던 것을 생각하면 된다.
이렇게 화면 정보(이하 화면 비디오 스트림)을 가져온 후 다른 사용자에게 이를 공유하기 위해서는 다른 peer 에게 보내는 Track 에 담기는 정보를 "나의 카메라 비디오 스트림" 에서 "화면 비디오 스트림" 으로 교체해야한다.
- 여기서 Track 이란 현재 MediaStream 을 구성하는 각 요소를 의미한다.
- Track 는 오디오, 비디오, 자막 총 3개의 stream 으로 구성된다. 때문에 Track 객체는 track[0] = 오디오, track[1] = 비디오 의 배열 구조로 되어있다
- MediaStream 이란 video stream 과 audio steam 등의 미디어 스트림을 다루는 객체를 이야기한다
- stream(스트림)이란 쉽게 생각하자면 비디오와 오디오 데이터라고 생각하자
정리하자면 상대방에게 보내는 track 에서 나의 웹캠 videoStream 대신 공유 화면에 해당하는 videoStream 으로 변경하면 상대방의 remoteVideo 가 그에 맞게 변경된다.
이렇듯 Track 에서 steam 을 교체 - replace - 하기 위해서는 아래와 같은 과정을 거친다. 아래 과정을 잘 생각하며 코드에 달린 주석을 참고하자!
1. myPeerConnection 에서 sender 를 가져온다. sender 란 나와 연결된 다른 peer 로 생각하면 된다.
2. sender 객체에서 replaceTrack 함수를 활용해서 stream 을 교체한다.
3. 이때 shareView 의 경우 VideoStream 의 경우 audio 까지 포함해서 화면 공유를 시작한 것이 아니라면 Track[0] 에 videoStream 이 들어있다. 때문에 replaceTrack 의 파라미터에 shareView.getTrack[0] 을 넣는다.
4. 화면 공유 취소 시 원래 화상 화면으로 되돌리기 위해서는 다시 Track 를 localstream 으로 교체해주면 된다! 이때 localStream 에는 audio 와 video 모두 들어가 있음으로 video 에 해당하는 Track[1] 만 꺼내서 교체해준다.
1-1. webrtc_client.js : 화면 공유
- 맨 위에 #view_on 과 #view_off 를 클릭했을 때 이벤트를 꼭 넣어주자. 각각 on 을 눌렀을 때 비디오 공유 시작, off 를 눌렀을 때 비디오 공유 중지를 의미한다.
- 위에 설명한 1~4 번 과정을 기억하자. 이것만 기억하고 응용하면 또 다르게 만들 수 있을듯하다.
/* 화면 공유를 위한 변수 선언 */
const screenHandler = new ScreenHandler();
let shareView = null;
/**
* ScreenHandler
* @constructor
*/
function ScreenHandler() {
// let localStream = null;
log('Loaded ScreenHandler', arguments);
// REF https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#Properties_of_shared_screen_tracks
const constraints = {
audio: true,
video: {
width: 1980, // 최대 너비
height: 1080, // 최대 높이
frameRate: 50, // 최대 프레임
},
};
/**
* 스크린캡쳐 API를 브라우저 호환성 맞게 리턴합니다.
* navigator.mediaDevices.getDisplayMedia 호출 (크롬 72 이상 지원)
* navigator.getDisplayMedia 호출 (크롬 70 ~ 71 실험실기능 활성화 or Edge)
*/
function getCrossBrowserScreenCapture() {
if (navigator.getDisplayMedia) {
return navigator.getDisplayMedia(constraints);
} else if (navigator.mediaDevices.getDisplayMedia) {
return navigator.mediaDevices.getDisplayMedia(constraints);
}
}
/**
* 스크린캡쳐 API를 호출합니다.
* @returns shareView
*/
async function start() {
try {
shareView = await getCrossBrowserScreenCapture();
} catch (err) {
log('Error getDisplayMedia', err);
}
return shareView;
}
/**
* 스트림의 트렉을 stop()시켜 스트림이 전송을 중지합니다.
*/
function end() {
shareView.getTracks().forEach((track) => {
// log("화면 공유 중지")
track.stop();
});
// // 전송 중단 시 share-video 부분 hide
// $("#share-video").hide();
}
/**
* extends
*/
this.start = start;
this.end = end;
}
/**
* screenHandler를 통해 스크린 API를 호출합니다
* 원격 화면을 화면 공유 화면으로 교체
*/
async function startScreenShare() {
// 스크린 API 호출 & 시작
await screenHandler.start();
// 1. myPeerConnection 에 연결된 다른 sender 쪽으로 - 즉 다른 Peer 쪽으로 -
// 2. shareView 의 Track 에서 0번째 인덱스에 들어있는 값 - 즉 videoStream 로 - 교체한다.
await myPeerConnection.getSenders().forEach((sender)=>{ // 연결된 sender 로 보내기위한 반복문
// 3. track 를 shareView 트랙으로 교체
sender.replaceTrack(shareView.getTracks()[0])
})
/**
* 화면 공유 중지 눌렀을 때 이벤트
*/
shareView.getVideoTracks()[0].addEventListener('ended', () =>{
// log('screensharing has ended')
// 4. 화면 공유 중지 시 Track 를 localstream 의 videoStram 로 교체함
myPeerConnection.getSenders().forEach((sender) =>{
sender.replaceTrack(localStream.getTracks()[1]);
})
// $("#share-video").hide();
});
// console.dir(shareView.getTracks());
// console.dir(localStream.getTracks());
}
/*
* video off 버튼을 통해 스크린 API 종료
* */
async function stopScreenShare(){
// screen share 종료
await screenHandler.end();
// myPeerConnection
await myPeerConnection.getSenders().forEach((sender) =>{
// 4. 화면 공유 중지 시 Track 를 localstream 의 videoStram 로 교체함
sender.replaceTrack(localStream.getTracks()[1]);
})
}
1-2. webrtc_client.js : peer connect, disconnect event
- 다른 peer 과 connect 되거나 disconnect 되었을 때 이벤트 처리
- 최종적으로 connect 되었을 때 remoteVideo 를 show 하거나 discconnect 되었을 때 remoteVideo 를 hide 시킨다.
- 문제는...!! remoteVideo 가 바로 안 사라지고 약 5초? 정도있다가 사라진다는 점.
function createPeerConnection() {
myPeerConnection = new RTCPeerConnection(peerConnectionConfig);
// event handlers for the ICE negotiation process
myPeerConnection.onicecandidate = handleICECandidateEvent;
myPeerConnection.ontrack = handleTrackEvent;
// the following events are optional and could be realized later if needed
// myPeerConnection.onremovetrack = handleRemoveTrackEvent;
myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
// myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
// myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
}
/** peerConnection 과 관련된 이벤트 처리
* 다른 peer 와 연결되었을 때 remote_video show 상태로로, 끊졌을때는 remote_video 를 hide 상태로 변경
* **/
function handleICEConnectionStateChangeEvent(){
let status = myPeerConnection.iceConnectionState;
if(status === "connected"){
log("status : "+status)
$("#remote_video").show();
}else if(status === "disconnected"){
log("status : "+status)
$("#remote_video").hide();
}
}
2. rtcroom.html
- view_on , view_off 버튼을 html에 넣어주었다.
<div class="mr-2" data-toggle="buttons">
<label class="btn btn-outline-success" id="view_on">
<input type="radio" name="options" style="display:none" autocomplete="off">view On
</label>
<label class="btn btn-outline-warning active" id="view_off">
<input type="radio" name="options" style="display:none" autocomplete="off">view Off
</label>
</div>
3. 코드 구현 확인
Reference : 꼭! 확인할 것
해당 사이트에서 정말정말 자세하게 설명되어있다.
https://webrtclab.herokuapp.com/screen-share/
webRTC 문서
https://developer.mozilla.org/ko/docs/Web/API/WebRTC_API
https://www.metered.ca/blog/webrtc-screen-sharing/
https://cryingnavi.github.io/webrtc/2020/10/15/webrtc-sharedscreen.html
Media Capture and Streams API 에 대한 설명
https://curryyou.tistory.com/443
webRTC peerDisconnect event
댓글