관련 프로퍼티
// RTCPeerConnection
// factory를 생성한 후에 factory에서 peer connection을 생성하는 구조
// factory 생성시에는 인코더, 디코더 필요
var factory:RTCPeerConnectionFactory
var peerConnection:RTCPeerConnection
// encoder, decoder
var encoderFactory:RTCDefaultVideoEncoderFactory
var decoderFactory:RTCDefaultVideoDecoderFactory
// delegate
var delegate:RTCPeerConnectionDelegate
// 카메라 캡처
var localVideoCapturer:RTCCameraVideoCapturer
// var localVideoCapturer:RTCFileVideoCapturer
// 미디어 스트림
var localStream:RTCMediaStream
// 로컬 트랙
var localVideoTrack:RTCVideoTrack
var localAudioTrack:RTCAudioTrack
// 원격지 트랙
var remoteVideoTrack:RTCVideoTrack
var remoteAudioTrack:RTCAudioTrack
Factory 생성
// 인코더, 디코더 생성
encoderFactory = RTCDefaultVideoEncoderFactory()
decoderFactory = RTCDefaultVideoDecoderFactory()
// 지원코덱
// RTCDefaultVideoEncoderFactory.supportedCodecs() 를 호출하면
// 지원하는 코덱의 정보를 RTCVideoCodecInfo 배열로 리턴한다.
for codecInfo in RTCDefaultVideoEncoderFactory.supportedCodecs() {
if( codecInfo.name.elementsEqual( "H264" ) {
encoderFactory.preferredCodec = codecInfo
break
}
}
// 인코더, 디코더를 사용해 PeerConnectionFactory 생성
factory = RTCPeerConnectionFactory( encoderFactory:encoderFactory, decoderFactory: decoderFactory)
PeerConnection 생성
// Ice 서버정보 배열
let iceServers:[RTCIceServer]
iceServers = [RTCIceServer.init( urlStrings: ["stun:stun.l.google.com:19302"])]
// connection constraints
let constraints:RTCMediaConstraints
let options:[String:String] = [
"DtlsSrtpKeyAgreement": kRTCMediaConstraintsValueTrue,
kRTCMediaConstraintsMaxWidth:"640",
kRTCMediaConstraintsMaxHeight:"480",
kRTCMediaConstraintsMaxFrameRate:"30"
]
constraints = RTCMediaConstraints.init( mandatoryConstraints: nil, optionalConstraints: options )
// config 설정
let config = RTCConfiguration()
config.iceServers = iceServers
config.iceTransportPolicy = .all
config.rtcpMuxPolicy = .negotiate
config.continualGatheringPolicy = .gatherContinually
config.bundlePolicy = .maxBundle
// connection 생성
peerConnection = factory.peerConnection( with: config, constraints: constraints, delegate: delegate)
로컬 미디어 스트림 생성
// 미디어 스트림 생성
let localStream:RTCMediaStream
localStream = (factory.mediaStream(withStreamId: "media"))
// 비디오 소스
var videoSource:RTCVideoSource = factory.videoSource()
// 카메라 캡처
#if TARGET_OS_SIMULATOR
localVideoCapturer = RTCFileVideoCapturer( delegate: videoSource )
#else
localVideoCapturer = RTCCameraVideoCapturer( delegate: videoSource )
#endif
// 로컬 트랙
localVideoTrack = factory.videoTrack( with: videoSource, trackId: "video0")
localAudioTrack = factory.audioTrack( withTrackId: "audio0" )
// View 생성
#if arch(arm64)
let rtcView:RTCMTLVideoView( frame: someYourView.bounds )
rtcView.videoContentMode = .scaleAspectFill
#else
let rtcView:RTCEAGLVideoView = RTCEAGLVideoView( frame: someYourView.bounds)
#endif
// 기존뷰, 트랙에 추가
someYourView.addSubview( rtcView )
localVideoTrack.add( rtcView )
// 로컬 스트림에 트랙 추가
localStream.addVideoTrack( localVideoTrack )
localStream.addAudioTrack( localAudioTrack )
// 커넥션에 스트림 추가
peerConnection.add( localStream )
// 데이터 채널 필요시
let config = RTCDataChannelCoinfiguration()
guard let dataChannel = self.peerConnection.dataChannel( forLabel: "DataCh", configuration: config) else {
return nil
}
dataChannel.delegate = self
오디오세션 설정
let rtcAudioSession = RTCAudioSession.sharedInstance()
rtcAudioSession.lockForConfiguration()
do {
try rtcAudioSession.setCategory( AVAudioSession.Category.playAndRecord.rawValue )
try rtcAudioSession.setMode( AVAudioSession.Mode.voiceChat.rawValue )
} catch let error {
}
rtcAudioSession.unlockForConfiguration()
시그널링을 위한 메시지 처리 : 시그널링은 규격이 따로 없으므로 서로 연결을 알고 데이터를 주고 받을 수 있는 형태로 임의 구성한다. candidate 정보나 sdp 정보를 주고 받을 수 있으면 된다.
Offer
시그널링 서버는 연결되어 있을테지만, 아직 피어간에 연결된 상태는 아니며, 내 접속 정보나 미디어 정보가 상대방에게 전달되어야 한다. 우선 내 정보를 획득하기 위해 offer 를 호출한다.(두 피어가 모두 offer를 보내는 것은 아니고, 서비스에 따라 한 피어가 offer를 먼저 보내도록 결정하면 된다.)
offer 를 호출하면 sdp를 생성하고 로컬 디스크립션으로 설정한다.
var mandatoryConstraints:[String:String] = [
kRTCMediaConstraintsOfferToReceiveVideo: kRTCMediaConstraintsValueTrue,
kRTCMediaConstraintsOfferToReceiveAudio: kRTCMediaConstraintsValueTrue
]
let constraints = RTCMediaConstraints( mandatoryConstraints: mandatoryConstraints, optionalConstraints:nil)
// offer
peerConnection.offer( for: mandatoryConstraints, completionHandler: { (sdp:RTCSessionDescription?, e:Error?) in
guard let sdp = sdp else {
return
}
self.peerConnection.setLocalDescription( sdp, completionHandler: { (error) in
// 시그널링 서버를 통해 sdp 전달
})
})
Answer
한쪽이 offer 를 전달해 오면 setRemoteDescription() 으로 상대방의 정보를 설정한다. 상대방에게 내 정보를 전달하기 위해 offer를 받은 피어는 answer 를 생성하고, 시그널링 서버를 통해 상대방에게 sdp를 전달한다.
// 원격지에서 시그널링서버를 통해 전달된 sdp를 설정
// 보통 json과 같은 string 형태로 전달된 sdp에서 RTCSessionDescription 생성
let remoteSdp = RTCSessionDescription( type: RTCSdpType.offer, sdp: jsonString)
// 해당 sdp를 설정 : set remote description
peerConnection.setRemoteDescription( remoteSdp, completionHandler: {(e:Error?) in
// 응답을 위한 answer sdp 생성하기
var mandatoryConstraints:[String:String] = [
kRTCMediaConstraintsOfferToReceiveVideo: kRTCMediaConstraintsValueTrue,
kRTCMediaConstraintsOfferToReceiveAudio: kRTCMediaConstraintsValueTrue
]
let constraints = RTCMediaConstraints( mandatoryConstraints: mandatoryConstraints, optionalConstraints:nil )
peerConnection.answer( for: constraints, completionHandler: { (sdp:RTCSessionDescription?, e:Error?) in
// 시그널링 서버를 통해 sdp 전달
})
})
offer 를 보낸 피어는 answer 를 받을 테니 최종적으로 setRemoteDescription() 을 해주면 sdp 교환은 완료된다. sdp 교환은 일단 상호 미디어와 송수신 설정을 위한 부분이고, 아직 p2p로 연결된 상황은 아니다.
RTCPeerConnectionDelegate
PeerConnection이 생성되면 미디어는 offer/answer를 통해 각각 설정이 이루어지는데, 미디어를 제외한 네트워크 정보는 별도로 제공해야 한다. 이 네트웍 정보들을 통해 상호간에 p2p로 연결할 ip, port, protocol을 결정하게 된다. 해당 이벤트는 PeerConnection 생성시 전달한 RTCPeerConnectionDelegate에서 처리한다.
위에 offer 로 sdp 를 만들고, setLocalDescription 을 호출하면, delegate에 대략 아래와 같은 순서로 이벤트가 전달된다.
시그널링 상태 변경 -> ice 수집 상태 변경 -> ice candidate 생성
ice candidate가 네트워크 정보인데, 이 정보를 시그널링 서버를 통해 상대방에게 전달해 주어야 한다.
(ice candidate는 가능한 네트워크가 모두 나타나므로 여러개가 생성된다.)
IceCandidate
// 새로운 ice 후보 발견
// -(void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate;
public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate ) {
let candidate = [
"candidate":candidate.sdp,
"sdpMid":candidate.sdpMid,
"sdpMLineIndex":candidate.sdpMLineIndex
] as [String: Any]
// 위 문자열을 json string 으로 변환 후 시그널링 서버를 통해 전달
}
시그널링 서버로부터 전달된 IceCandidate 를 추가
// json스트링을 파싱
let jsonObject = 파싱루틴
let candidate = RTCIceCandidate(
sdp: jsonObject["candidate"] as! String,
sdpMLineIndex: jsonObject["sdpMLineIndex"] as! Int32,
sdpMid: jsonObject["sdpMid"] as! String)
peerConnection.add( candidate )
상호간에 ice candidate를 주고 받고 설정하면, p2p 연결이 이루어지게 된다.
ice 연결되면 캡처 시작 및 데이터 전송
// ice 연결상태 변경
// -(void)peerConnection:(RTCPeerConnection *)peerConnection didChangeIceConnectionState:(RTCIceConnectionState)newState;
public func peerConnection(_ peerConnection: RTCPeerCopnnection, didChange newState: RTCIceConnectionState ) {
if( newState == RTCIceConnectionState.connected {
// 로컬 비디오 캡처를 시작한다
if let capturer = self.localVideoCapturer {
// 캡처 장치 설정 및 캡처 시작
}
} else if ( newState == .failed {
} else if ( newState == .disconnected {
} else if ( newState == .completed ) {
} else if ( newState == .closed ) {
}
}
캡처 장치 설정 및 캡처 시작
각 플랫폼별로 독립적으로 구성된 부분으로 캡처 장치를 설정하고 RTCCameraCapturer 로 장치 정보와 포맷을 전달한다
// 전면, 후면 위치 선택
let position = AVCaptureDevice.Position.front;
// 캡처 장치 검색
let captureDevice:AVCaptureDevice
for device:AVCaptureDevice in AVCaptureDevice.devices(for: AVMediaType.video) {
if( device.position == position ) {
captureDevice = device
break;
}
}
// 포맷 설정
let targetWidth = Int32( 640 )
let targetHeight = Int32( 480 )
var selectedFormat:AVCaptureDevice.Format
var currentDiff = INT_MAX
let formats = RTCCameraVideoCapturer.supportedFormats( for: captureDevice )
for format in formats {
let dimension:CMVideoDimensions = CMVideoFormatDescriptionGetDimensions( format.formatDescription )
let pixelFormat:FourCharCode = CMForamtDescriptionGetMediaSubType( format.formatDescription )
let diff = abs(targetWidth - dimension.width) + abs( targeHeight - dimension.height)
if( diff < currentDiff ) {
selectedFormat = format
currentDiff = diff
} else if( diff == currentDiff && pixelFormat == capturer.perferredOutputPixelFormat()) {
selectedFormat = format
}
})
var maxFramerate:Float64 = 0.0
for fpsRange in selectedFormat.videoSupportedFrameRateRanges {
maxFramerate = fmax( maxFramerate, fpsRange.maxFrameRate )
}
let fps = Int(maxFramerate)
capturer.startCapture( with: captureDevice, format: selectedFormat, fps: fps )
RTCDataChannelDelegate
func dataChannelDidChangeState(_ dataChannel: RTCDataChannel ) {
}
func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) {
}
'프로그래밍 > WebRTC' 카테고리의 다른 글
[WebRTC] peerConnection.statistics() 중 자주 확인하는 데이터 (0) | 2022.06.20 |
---|---|
[WebRTC] 안드로이드 I420 Buffer관련 클래스 (0) | 2020.04.22 |
[WebRTC] android camera 관련 (2) | 2020.02.29 |
[WebRTC] 사이멀캐스트? 네이티브를 위한 레거시 simulcast (0) | 2020.01.01 |
[WebRTC] iOS Audio 관련 클래스 (0) | 2019.10.10 |
[WebRTC] Android SurfaceTextureHelper (21) | 2019.07.16 |
WebRTC native build (0) | 2019.05.22 |