WebRTC

[WebRTC] WebRTC를 공부하며 스트리밍 과정 익히기

GaGah 2021. 1. 16. 03:35

 

유튜브 플랫폼에서 구독자와 소통하기 위해 라이브 방송을 킨다고 가정해보자.
스트리밍을 하기 위해 가장 먼저 해야하는 것은 웹/앱에 연결된 카메라, 마이크 등에 연결을 해야한다.
(카메라, 마이크가 연결되지 않으면 스트리밍을 하는 의미가 없어진다.)

그렇다면, 어떻게 연결할 수 있을까?
연결과정을 알아보자.

 

1. WebRTC와 미디어 장치

1-1. 미디어 장치와 연결하기 

[1] 제약조건에 맞는 카메라, 마이크를 선택해준다.

const openMediaDevices = async (constraints) => {
    return await navigator.mediaDevices.getUserMedia(constraints);
}

try {
    const stream = openMediaDevices({'video':true,'audio':true});
    console.log('Got MediaStream:', stream);
} catch(error) {
    console.error('Error accessing media devices.', error);
}
  • navigator.mediaDevices 객체를 통해서 해당 기기에 연결되어 있는 장치들을 불러온다.

  • getUserMedia(constraints) : 연결된 장치들 중 constraints에 만족하는 장치들을 불러온다.

    • 정상적으로 호출한다면 권한 요청

      • 권한 수락 : MediaStream이 연결됨

      • 권한 거부 : PermissionDeniedError 발생

    • 연결된 장치가 없다면, NotFoundError 발생

  • MediaDevices의 인터페이스(또는 예외상황-중간에 장치들의 연결이 끊김 등)

 

[2] 사용자가 직접 고르도록 하기

async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

const videoCameras = getConnectedDevices('videoinput');
console.log('Cameras found:', videoCameras);

 

[3] 장치가 변경되었을 경우

USB로 연결된 웹캠을 사용하고 있다가, 연결이 끊겨서 노트북에 내장된 웹캠을 사용할 때, 미디어 장치의 변경 사항 수신한다.

// Updates the select element with the provided set of cameras
function updateCameraList(cameras) {
    const listElement = document.querySelector('select#availableCameras');
    listElement.innerHTML = '';
    cameras.map(camera => {
        const cameraOption = document.createElement('option');
        cameraOption.label = camera.label;
        cameraOption.value = camera.deviceId;
    }).forEach(cameraOption => listElement.add(cameraOption));
}

// Fetch an array of devices of a certain type
async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

// Get the initial set of cameras connected
const videoCameras = getConnectedDevices('videoinput');
updateCameraList(videoCameras);

// Listen for changes to media devices and update the list accordingly
navigator.mediaDevices.addEventListener('devicechange', event => {
    const newCameraList = getConnectedDevices('video');
    updateCameraList(newCameraList);
});

 

1-2. 미디어 제약사항

[1] constraints 객체를 통해 요구사항 설정 가능

  • 구체적 또는 느슨하게 정의할 수 있다.

  • ex ) 해상도, 장치ID, 기존 장치인지 확인하는 deviceId, 마이크 에코, 최소 너비 및 높이 설정 등

 

[2] constraints 객체는 MediaStreamConstraints 인터페이스를 상속한다.

developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints

 

 

1-3. 로컬 재생

카메라, 마이크 등에 연결을 하고 제약사항 등을 설정한 후 실제 방송을 키게 되면, 내 방송이 어떻게 스트리밍 되고 있는지 로컬에서 재생하고 있는 화면이 보인다. 

async function playVideoFromCamera() {
    try {
        const constraints = {'video': true, 'audio': true};
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        const videoElement = document.querySelector('video#localVideo');
        videoElement.srcObject = stream;
    } catch(error) {
        console.error('Error opening video camera.', error);
    }
}

 

2. 미디어 캡처 및 제약

2-1. 미디어 제약 조건 

[1] 미디어 장치에 액세스 할 때 가능한 자세한 제약 조건을 제공하는 것이 좋다.

애플리케이션 용도에 가장 적합한 미디어 스트림(마이크, 카메라)을 찾기 위해서

 

[2] 특정 제약 조건은 MediaTrackConstraint 개체에 정의되어 있다. 

 

정확한 값을 요구하는 경우

// Camera with the exact resolution of 1024x768
{
    "video": {
        "width": {
            "exact": 1024
        },
        "height": {
            "exact": 768
        }
    }
}

 

2-2. 디스플레이 미디어

  • 화면을 캡처하고 녹화를 하려면 Display Media API를 사용해야한다.

  • navigator.mediaDevices 의 getDisplayMedia()를 통해 디스플레이의 내용을 여는 데 사용된다. 

{
    video: {
        cursor: 'always' | 'motion' | 'never',
        displaySurface: 'application' | 'browser' | 'monitor' | 'window'
    }
}

 

2-3. 스트림 및 트랙

 MediaStream은 오디오, 비디오의 트랙(MediaStreamTrack)으로 구성된 미디어 콘텐츠 스트림을 말한다.MediaStream.getTracks()은 MediaStreamTrack 객체의 배열을 반환한다.

 

2-3-1. MediaStreamTrack

  • MediaStreamTrack은 kind(종류) 속성이 있고, audio인지 video인지를 구별한다.

  • enabled : 트랙을 음소거 할 수 있다.

  • remote : RTCPeerConnection을 하고 remote peer로 부터 왔는지를 판단하는 Boolean type

 


이제 영상을 송출할 준비는 끝났다!

내가 지금 찍고 있는 이 영상을 다른 곳에 보내기 위해서 대상이 되는 PEER(클라이언트)와 연결을 시켜주어야 한다. 

 

 

3. Peer Connection

두 피어가 연결을 할 수 있으려면 두 클라이언트 모두 ICE 서버 구성을 제공해야한다.

이를 STUN, TURN 서버가 remote peer와 연결할 수 있는 ICE 후보들을 제공한다.

ICE 후보들을 전송하는 것을 "Signaling(시그널링)"이라고 부른다. 

 

3-1. Signaling

  • WebRTC 사양에 ICE서버와 통신하기 위한 API가 포함되어 있지만, Signaling의 일부는 아니다.

  • Signalling은 어떻게 서로 연결될 수 있는지 공유하기위해 필요하다.

    • 예를 들어 A는 어떤 대상에게 편지를 보낼 때 박스로 동봉해서 주는 방식, 편지봉투에 담아서 보내는 방식 등등 어떤 방식으로 보낼지 연결할지 공유한다. 

  • Signalling은 피어 연결이 이루어지기 전 웹 애플리케이션이 필요한 정보를 전달할 수 있는 HTTP 기반의 웹 API로 이루어진다.

// 비동기적으로 메세지를 주고 받는 방식
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
    // New message from remote client received
});

// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');

 

cf) ICE는 두 단말이 서로 통신할 수 있는 최적의 경로를 찾을 수 있도록 도와주는 프레임워크이다. STUN과 TURN을 활용하며 SDP 제안 및 수락 모델을 적용할 수 있다.

 

3-2. Peer Connection 시작

각 피어간의 연결은 RTCPeerConnection 객체에 의해 다뤄진다.

  • RTCPeerConnection의 생성자는 RTCConfiguration객체 하나를 매개변수로 가진다.

    • RTCConfiguration 객체는 어떻게 peer연결을 설정할지, 사용할 ICE 서버의 정보를 포함해야한다. 

  • RTCPeerConnection은 송신/수신을 할지에 따라 SDP offer/answer을 만들어야한다.

  • SDP offer/answer이 생성되면 다른 채널을 통해 remote peer에 보내야한다.

  • SDP object를 remote peer에 보내주는 것을 Signalling이라고 한다.

    • WebRTC 사양에는 포함되지 않음

3-2-1. 송신자( 스트리머 )

  • 송신자(스트리머)가 연결을 시작하기 위해, RTCPeerConnection 객체를 만들고 createOffer()을 호출하면  RTCSessionDescription 객체가 생성된다.

    • RTCSessionDescription 은 setLocalDescription()함수를 사용해서 로컬을 설정하고 수신측의 signaling채널로 보낸다.

  • 또한, Signaling채널에 listener를 만들고 우리가 보냈던 세션 정보를 수신측에서 수신을 잘 받았다는 답변을 받는다.

async function makeCall() {
    const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
    const peerConnection = new RTCPeerConnection(configuration);
    signalingChannel.addEventListener('message', async message => {
        if (message.answer) {
            const remoteDesc = new RTCSessionDescription(message.answer);
            await peerConnection.setRemoteDescription(remoteDesc);
        }
    });
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    signalingChannel.send({'offer': offer});
}

 

 

3-2-2. 수신자( Viewer )

  • 수신 측에서는 offer가 오기 전까지 RTCPeerConnection 인스턴스를 만들지 않는다. 

  • offer가 오면 setRemoteDescription()함수로 연결을 성립한다. 

  • 그 다음으로 createAnswer()을 만들어서 송신 측에 잘 받았다는 응답을 보낸다.

    • 만들어진 answer로 local description을 셋팅하고(setLocalDescription() 함수 사용) 시그널링 서버를 통해 송신 측에 answer을 보낸다.

const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
    if (message.offer) {
        peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        signalingChannel.send({'answer': answer});
    }
});
  • 양쪽의 피어가 로컬과 원격 세션 정보를 세팅하면 원격 피어의 기능을 알 수 있다.

  • 아직 두 피어간의 연결이 된 것은 아니다.

  • 피어 간의 연결이 완료되기 위해서는 각 피어에서 ICE 후보들을 수집해 서로에게 전송해야 한다.

 

3-3. ICE candidates

  • 두 피어들이 WebRTC를 사용해서 데이터 전송, 수신 등을 하기 전에, 연결정보를 교환해야한다. 

  • 네트워크 상태가 여러 요인에 의해 달라지기 때문에 연결할 수 있는 후보자들을 찾아주는 역할을 할 외부 서비스가 필요하다. 

    • 그것이 바로 ICE!

      • ICE는 STUN, TURN 서버를 사용한다.

      • STUN(Session Traversal Utilities for NAT)서버는 대부분 WebRTC 애플리케이션에서 종종 사용한다.

      • TRUN(Traversal Using Relay NAT)는 STUN 프로토콜을 통합한 발전된 솔루션으로 대부분의 상용 WebRTC 서비스들이 사용한다.

  • WebRTC API는 STUN과 TURN을 지원해준다.

  • WebRTC로 연결을 할 때, 보통 한 개 이상의 ICE 서버들을 RTCPeerConnection객체에 담아서 제공한다. 

 

3-4. Trickle ICE

  • RTCPeerConnection객체가 만들어지면 제공된 ICE서버들을 사용해서 candidates들을 모은다.

  • RTCPeerConnection의 icegatheringstatechange이벤트는 ICE gathering이 (new, gathering, complete) 인지 상태를 나타낸다.

  • 피어에 ICE 수집이 완료되기 전까지 대기하라고 하는 것이 가능하지만, 보통 "trickle ice" 테크닉을 사용해 각 ICE 후보가 발견이 될 때마다 remote 피어에 전달하는 것이 효율적이다.

  • Trickle ICE 테크닉은 피어 간의 연결 설정 시간을 줄여 비디오가 적은 딜레이로 시작할 수 있게 해준다.

  • ICE 후보를 수집하려면, icecandidate 이벤트를 listener에 추가하면 된다. 해당 리스너에서 방출된 RTCPeerConnectionIceEvent는 remote 피어에게 전송될 새로운 후보를 포함한 candidate 속성을 갖는다.

// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
    if (event.candidate) {
        signalingChannel.send({'new-ice-candidate': event.candidate});
    }
});

// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
    if (message.iceCandidate) {
        try {
            await peerConnection.addIceCandidate(message.iceCandidate);
        } catch (e) {
            console.error('Error adding received ice candidate', e);
        }
    }
});

 

 

3-5. 연결설정

ICE 후보들이 수신되면, peer의 연결상태가 연결된 상태로 변경된다. 

연결된 상태로 변한 것을 알기 위해 connectionstatechange 를 수신하는 RTCPeerConnction에 리스너를 추가하면 된다.

// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
    if (peerConnection.connectionState === 'connected') {
        // Peers connected!
    }
});

 

 

4. 원격 스트림 시작하기

RTCPeerConnection이 remote peer에 연결이 되면, 비디오 또는 오디오를 스트리밍 할 수 있다.

미디어 스트림은 적어도 하나의 미디어 track이 있어야하고 각 트랙들은 원격 피어에게 미디어를 전송하고 싶을 때 RTCPeerConnection에 개별적으로 더해진다.

const localStream = await getUserMedia({vide: true, audio: true});
const peerConnection = new RTCPeerConnection(iceConfig);
localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
});

트랙은 remote peer에 연결되기 전에 RTCPeerConnection에 추가할 수 있다. 이렇기 때문에 연결될 때까지 기다리지 않고 먼저 설정해주는 편이 낫다. 

 

 

4-1. 원격 트랙 추가

다른 피어가 추가한 원격 트랙을 받기 위해서, track 이벤트를 받을 리스터를 추가해줘야 한다. 

MediaStream객체에서 재생이 되기 때문에, 먼저 빈 인스턴스를 만든 다음 트랙을 수신할 때 원격 피어의 트랙으로 채운다.

 

 

 

 

참고 자료

webrtc.org/getting-started/overview?hl=en

webrtc.org/getting-started/media-capture-and-constraints?hl=ko

RTCPeerConnection 관련 MDN 문서 :  developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection

 

RTCPeerConnection - Web APIs | MDN

The RTCPeerConnection interface represents a WebRTC connection between the local computer and a remote peer. It provides methods to connect to a remote peer, maintain and monitor the connection, and close the connection once it's no longer needed. EventTar

developer.mozilla.org

 

LIST