본문 바로가기

프로그래밍/WebRTC

[WebRTC] 사이멀캐스트? 네이티브를 위한 레거시 simulcast

 * 개인적으로 simulcast를 위한 iOS, Android 클라이언트를 구성하면서 판단한 경험에 대한 내용이라 실제 표준과 다를 수 있습니다.

* 추가) 레거시 planB 는 2021년 12월말에 크롬에서 삭제될 예정

 

 현재 웹 환경(브라우저)에서 지원하는 simulcast의 경우 unified plan기반으로 tranceiver 를 사용해 불필요한 SDP 정의를 최소화하는 것으로 변화되었다. 별다른 설정없이 tranceiver 설정에 encoding parameter만 설정하면, SDP 에 아래와 같은 문자열이 추가된다.
a=rid:label1 send
a=simulcast:send label1;lable2;label3 
webrtc 라이브러리는 로컬, 리모트 SDP가 위와 같이 설정되면 로컬 송출 트랙(sender)에 3개의 레이어를 설정하게 되고, 레이어마다 다른 크기로 인코딩을 진행하게 된다. 야누스와 같은 SFU 서버 역시 원격지의 SDP 설정에 따라 여러 레이어를 전달받고, 해당 스트림을 수신하는 측에 원하는 레이어를 선택할 수 있는 기능을 제공하게 된다.

사이멀캐스트 관련해서는 총 3가지의 표준(?)이 존재한다.
첫번째 초기 구글 크롬에서 사용하던 planB 로 각 레이어에 대해 SSRC 그룹을 지정하는 방식으로 현재 레거시로 분류되어 있다.
두번째 파이어폭스에서 사용하던 현재의 unified plan 과 기존 ssrc가 혼합된 형태로 파이어폭스에서만 사용되었기에 상호 호환을 위해 레거시 형태로 변경해 사용되곤 했다.
세번째는 현재의 unified plan 형태이다. webrtc 표준에 의해 향후 unified plan으로 통일되었고, 크롬, 파이어폭스, 사파리 모두 지원하는 것으로 알고 있다.

당연히 표준인 세번째 unified plan을 사용하면 되는데...... 네이티브 환경에서는 이것저것 설정해봐도 정상 동작하지 않고, 레거시 환경을 사용해야 하는 상황이 생겼다. ㅠㅠ
네이티브 환경인 iOS, Android에서도 unified plan 및 tranceiver를 지원하고 있다. 동일한 구성시 simulcast에 문제가 없어야 하는데.. 인코더 쪽에 오류가 나면서 정상적으로 비디오 스트림을 생성하지 못하는 문제가 발생되었다.

결국, 네이티브에서는 simulcast 지원을 위해 어쩔수 없이 planB 레거시 simulcast를 적용해보는 수밖에는 방법이 없는 상황. 
( 네이티브 환경에서의 unified plan의 동작 여부에 대해 오피셜한 내용으로 확인한 것이 아니라 그냥 삽질상 정상 동작이 안되는 것이므로 정확히 안된다고 말할 수가 없음)

 레거시 simulcast를 적용하기 전에 사용하는 SFU 에서 어떤 종류를 지원하고 있는지 확인해야 한다. WebRTC 1.0 표준이 정의되고, simulcast가 언급된 이후로는 대부분 첫번째 레거시 방식과 unified plan 방식을 지원하고 있는 것으로 알고 있다. SDP에 대략 아래와 같은 내용의 정의가 추가된다.  일반적인 경우에는 ssrc에는 미디어 서버 아이디와 트랙의 아이디 정으로 끝나게 되는데, 비디오 simulcast를 위해 ssrc 그룹으로 SIM 과 각 레이어마다 FID를 지정한다.

a=ssrc-group:SIM 3097377275 1580610016 1486063784
a=ssrc-group:FID 3097377275 1433109858
a=ssrc-group:FID 1580610016 2370627631
a=ssrc-group:FID 1486063784 302605494
a=ssrc:3097377275 cname:RUbUEFCaiXpA1\\\/k3
a=ssrc:3097377275 msid:VideoStream-0 VideoStream-0
a=ssrc:3097377275 mslabel:VideoStream-0
a=ssrc:3097377275 label:VideoStream-0
a=ssrc:1580610016 cname:RUbUEFCaiXpA1\\\/k3
a=ssrc:1580610016 msid:VideoStream-0 VideoStream-0
a=ssrc:1580610016 mslabel:VideoStream-0
a=ssrc:1580610016 label:VideoStream-0
a=ssrc:1486063784 cname:RUbUEFCaiXpA1\\\/k3
a=ssrc:1486063784 msid:VideoStream-0 VideoStream-0
a=ssrc:1486063784 mslabel:VideoStream-0
a=ssrc:1486063784 label:VideoStream-0
a=ssrc:1433109858 cname:RUbUEFCaiXpA1\\\/k3
a=ssrc:1433109858 msid:VideoStream-0 VideoStream-0
a=ssrc:1433109858 mslabel:VideoStream-0
a=ssrc:1433109858 label:VideoStream-0
a=ssrc:2370627631 cname:RUbUEFCaiXpA1\\\/k3
a=ssrc:2370627631 msid:VideoStream-0 VideoStream-0
a=ssrc:2370627631 mslabel:VideoStream-0
a=ssrc:2370627631 label:VideoStream-0
a=ssrc:302605494 cname:RUbUEFCaiXpA1\\\/k3
a=ssrc:302605494 msid:VideoStream-0 VideoStream-0
a=ssrc:302605494 mslabel:VideoStream-0
a=ssrc:302605494 label:VideoStream-0

 

 실제로 위의 SDP 문자열을 직접 추가하고 기존 비디오 정보를 수정해야 하는 것은 아니다. 다행스럽게도 webrtc 라이브러리에서는 media constraints 설정을 통해 SDP 설정을 지원하고 있다. Offer SDP 생성시 아래와 같이 레거시 모드로 설정하게 되면 정상적으로 simulcast가 동작하게 된다.

- planB : 레거시이므로  SDP의 sementic 설정을 planB로 설정.
- offer 생성시 media constraints 옵션에 googNumSimulcastLayers 를 3으로 설정.
- peer connection factory 생성시 software encoder 사용.

// iOS swift PeerConnection 설정 예
var encoderFactory = RTCDefaultVideoEncoderFactory()
var decoderFactory = RTCDefaultVideoDecoderFactory()
var factory = RTCPeerConnectionFactory(encoderFactory: encoderFactory, decoderFactory: decoderFactory)

let rtcConfig = RTCConfiguration()
rtcConfig.iceServers = iceServers // iceServers:[RTCIceServer]
rtcConfig.icetransportPolicy = .all
rtcConfig.rtcpMuxPolicy = .negotiate
rtcConfig.continualGatheringPolicy = .gatherContinually
rtcConfig.bundlePolicy = .maxBundle
rtcConfig.sdpSemantics = RTCSdpSemantics.planB

var optionalConstraints:[String:String] = [:]
optionalConstraints["DtlsSrtpKeyAgreement"] = "true"

let constraints = RTCMediaConstraints.init(mandatoryConstraints:nil, optionalConstraints: 

var peerConnection = factory.peerConnection(with: rtcConfig, constraints: constraints, delegate: delegate )


기존, planB에서는 스트림을 사용했는데,  이를 unified plan 처럼 트랙으로 변경.
(unified plan에서는 스트림을 사용하지 않고, Sender, Receiver 기반으로 동작하는데, planB에서도 같은 구성을 사용할 수 있는듯...)

// 기존
let stream = factory.mediaStream(withStreamId: "streamId"))
stream.addAudioTrack(audioTrack)
stream.addVideoTrack(videoTrack)
peerConnection.add(stream)


// 변경
peerConnection.add(audioTrack, ["streamId"])
peerConnection.add(videoTrack, ["streamId"])

 

SDP 생성

// iOS swift offer 설정 예
var mandatoryConstraints:[String:String] = [:]
mandatoryConstraints["OfferToReceiveAudio"] = "false"
mandatoryConstraints["OfferToReceiveVideo"] = "false"
mandatoryConstraints["googNumSimulcastLayers"] = "3"

let constraints = RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints:nil)

peerConnection.offer( for: constraints, completionHandler: { (sdp:RTCSessionDescription?, e:Error?) in

})

 

 삽질한 시간에 비하면 참으로 설정이 간단해서 허탈할 정도. 특히 하드웨어 인코딩을 지원하는 안드로이드 단말에서 하드웨어 인코더(DefaultVideoEncoder)를 사용하는 경우 다중 레이어를 위한 인코딩을 지원하지 않는다. 이 경우 특정 레이어 하나만 선택되어 인코딩 되므로 주의해야 한다. 

 추가로 브라우저는 최근의 unified plan을 이미 지원하고 있어 기기에 설치한 크롬에서도 정상 동작해야 하는데.. 테스트 설정에 문제가 있었는지...아무튼 모바일 크롬에서는 simulcast가 정상 동작하지 않았다. 이 부분은 좀 더 확인을 해 보아야 하는 부분으로 추후 관련 내용을 덧 붙일 예정이다. 

 모바일 환경에서 simulcast 송출을 한다는것은 여러가지 리스크가 있다. 우선 배터리를 사용하는 모바일 기기에서 세개의 인코딩을 진행하는 경우 배터리 사용과 cpu사용량이 눈에 띄게 늘어난다. 요새 코어수가 대부분 쿼드 이상이고, 게임하면서 화면 인코딩+방송까지 하는 시대다보니 simulcast의 경우에도 막 100% 근접하는 등의 극악한 사용률을 보이진 않는다. cpu부하는 대략 30% 중후반의 사용률을 보이는 것 같은데... 문제는 발열과 배터리 광탈이다. (따듯해진 폰이 겨울에 핫팩 대용?)
 더욱 문제가 되는 부분은 그나마 wifi에서는 문제가 없는데, 모바일 환경에서의 방송 송출의 경우 가뜩이나 불안한 통신환경상 세개의 인코딩 데이터를 안정적으로 전송하는 것을 방해하는 요인이 너무 많다는 것이다. 각 레이어마다 네트워크 상황에 따라 추가로 scale 의 변경이나 프레임 드랍이 발생하는데, 이 경우 다른 스트리밍 방식보다 시청자의 사용자 경험을 해치게 된다. (사실 이 부분은 단순 품질보다 레이턴시를 중요하게 생각하는 webrtc의 특징이라고 할 수 있음)

 

 

일단 최신버전에서는 동작할 수도 있으니, unified plan 의 사이멀캐스트는 대략 아래처럼 설정된다.

let audio = RTCRtpTransceiverInit()
let video = RTCRtpTransceiverInit()

audio.direction = RTCRtpTransceiverDirection.sendOnly
video.direction = RTCRtpTransceiverDirection.sendOnly

// 오디오 트랙은 차이가 없으므로, 그냥 추가
let audioTranceiver = peerConnection.addTransceiver(with: audioTrack, init: audio)


// 비디오
let low = RTCRtpEncodingParameters()
let mid = RTCRtpEncodingParameters()
let high = RTCRtpEncodingParameters()

low.isActive = true
low.rid = "l"
low.scaleResolutionDownBy = 4.0
low.maxBitrateBps = 100_000
low.networkPriority = 1.0

mid.isActive = true
mid.rid = "m"
mid.scaleResolutionDownBy = 2.0
mid.maxBitrateBps = 500_000
mid.networkPriority = 1.0

high.isActive = true
high.rid = "h"
high.scaleResolutionDownBy = 1.0
high.maxBitrateBps = 2_000_000
high.networkPriority = 1.0

video.sendEncodings = [low, mid, high]

let videoTransceiver = peerConnection.addTransceiver(with: videoTrack, init: video)

offer, answer를 위한 constraints 설정등은 planB와 동일하다. 
offer 는 원하는 설정으로 넣음 되는데... offer를 전달받은 경우에는?
offer를 setRemoteDescription()으로 설정하면 내부적으로 trancevier 들이 생성되므로, 생성된 trancevier들의 정보를 변경해 처리 하는 등의 좀 구찮은 작업들이 포함될 수 있다.

아무튼~ 인코딩시에 크래시가 발생해서 더 많은 테스트는 해보질 않았으나,  코덱이나 하드웨어 인코더 문제일 것으로 생각된다. 일단 기존 방식으로 동작하는지를 확인해보고 하는 마음으로 작성된 내용이었다.
최근 webrtc 빌드를 바탕으로 unified plan 으로도 정상 동작 여부를 확인해 봐야 한다.