原文地址
WebRTC是一个面向iOS和安卓的高层级的API的开源项目.
在这篇文章我们将研究如何开始建立WebRTC到你的iOS程序. 我们建议使用本地的WebRTC库,但你可以看看OpenWebRTC项目. 我们不会讲述如何建立使用signaling ,而是突出在iOS浏览器实现的异同. 正如你将看到的,这个API是在iOS和web上是平行的.如果你正在寻找一个更基本的WebRTC概论,我们推荐Sam Dutton’s Getting started with WebRTC. 本文中的代码并不完全,我们建议可以看看WebRTC团队的AppRTCDemo实现.
你可以自行下载源码进行十数小时的编译, 也可以直接pod libjingle_peerconnection
到你的项目.
建议你花些时间熟悉他们的头文件,下面的代码基于WebRTC 9103版本.
如果你想在外网使用,则需要一个STUN 或 TURNserver把两方连接起来. 你也可以忽略,当你在内网进行测试的时候.
RTCIceServer *iceServer = [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:YOUR_SERVER] username:USERNAME_OR_EMPTY_STRING password:PASSWORD_OR_EMPTY_STRING]];
iOS的库使用代理模式进行JavaScript API的回调. 它提供了几个代理为你实现一个完整的WebRTC程序. 如果仅仅是演示和运行 ,则需要 RTCPeerConnectionDelegate
和RTCSessionDescriptionDelegate
RTCPeerConnectionDelegate: 是web的 RTCPeerConnection.onNN 回调在ObjC中的实现:它们中许多方法你并不是必须的,RTCSessionDescriptionDelegate我们列举必要的两条出来:
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didCreateSessionDescription:(RTCSessionDescription *)sdp
error:(NSError *)error;
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didSetSessionDescriptionWithError:(NSError *)error;
即对应JavaScript中的
peerConnection.createOffer(function didCreateSessionDescription(sdp) {
peerConnection.setLocalDescription(sdp, function didSetSessionDescription() {
});
});
// 简单表示就是创建offer 后, 第一个方法进行localDescription设置. 第二个 发送sdp.
你们都知道,RTCPeerConnection是webRTC中用来控制媒体会话的.iOS也尽力模仿让他来管理你的会话.
使用RTCPeerConnectionFactory:来创建RTCPeerConnection
// Enable SSL globally for WebRTC in our app
[RTCPeerConnectionFactory initializeSSL];
RTCPeerConnectionFactory *pcFactory = [[RTCPeerConnectionFactory alloc] init];
// Create the peer connection using the ICE server list and the current class as the delegate RTCPeerConnection *peerConnection = [pcFactory peerConnectionWithICEServers:iceServers constraints:nil delegate:self];
你可以看到, 创建RTCPeerConnection非常类似web中的方式,所以,你也应该禁用SSL,当你完成了你的WebRTC电话,或者在应用程序终止时:
[RTCPeerConnectionFactory deinitializeSSL];
在这个示例中,我们要用RTCMediaStream
包装音频和视频,不像在web中,我们有getUserMedia ,我们现在需要自己创建stream对象. 这并不是很困难.
请注意,下面的代码假定用户授予访问摄像头和麦克风,在实际中这可能并非如此。
RTCMediaStream
包括了视频和音频的track. 我们可以添加每个不同的type,也可以不添加,直接创建stream然后添加一个one audio track和one video track.
RTCMediaStream *localStream = [factory mediaStreamWithLabel:@”someUniqueStreamLabel”];
获取audio track是容易的,只有一个麦克风来源,会自动获取,当你使用下面代码时:
RTCAudioTrack *audioTrack = [factory audioTrackWithID:@”audio0”]; [localStream addAudioTrack:audioTrack];
而Video track需要知道我们想使用的AVCaptureDevice
,在大多数iOS设备上你可以选择前后摄像头.下面让我们使用前置摄像头.
记住,你没有访问摄像头在模拟器上,所以下面的代码不会找到一个AVCaptureDevice除非你在设备上运行它。
// Find the device that is the front facing camera
AVCaptureDevice *device;
for (AVCaptureDevice *captureDevice in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] ) {
if (captureDevice.position == AVCaptureDevicePositionFront) {
device = captureDevice;
break;
}
}
// Create a video track and add it to the media stream
if (device) {
RTCVideoSource *videoSource;
RTCVideoCapturer *capturer = [RTCVideoCapturer capturerWithDeviceName:device.localizedName];
videoSource = [factory videoSourceWithCapturer:capturer constraints:nil];
RTCVideoTrack *videoTrack = [factory videoTrackWithID:videoId source:videoSource];
[localStream addVideoTrack:videoTrack];
}
最后 ,我们有了一个RTCMediaStream
的视频和音频轨道,现在是则需要添加到peer connection
.
[peerConnection addStream:localStream];
你可以发送或接受一个Offer
,这是使用signaling机制的逻辑. 现在假设你已经向对方成功发送了一个offer
.
RTCMediaConstraints *constraints = [RTCMediaConstraints alloc] initWithMandatoryConstraints:
@[
[[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],
[[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]
]
optionalConstraints: nil];
[peerConnection createOfferWithConstraints:constraints];
创建offer之后,就会用代理实现回调机制 ,让我们进行 localDescription设置:
- (void)peerConnection:(RTCPeerConnection *)peerConnection didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error { [peerConnection setLocalDescription:sdp] }
同样,之后是让我们进行发送offer(sdp)的回调.
- (void)peerConnection:(RTCPeerConnection *)peerConnection didSetSessionDescriptionWithError:(NSError *)error { if (peerConnection.signalingState == RTCSignalingHaveLocalOffer) { // Send offer through the signaling channel of our application } }
之后,你应当从对方获得一个answer
.来进行RemoteDescription设置.
RTCSessionDescription *remoteDesc = [[RTCSessionDescription alloc] initWithType:@"answer" sdp:sdp];
[peerConnection setRemoteDescription:remoteDesc];
但是目前只有处理设置本地描述。让我们扩展它来处理设置远程描述:
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didSetSessionDescriptionWithError:(NSError *)error
{
// If we have a local offer OR answer we should signal it
if (peerConnection.signalingState == RTCSignalingHaveLocalOffer | RTCSignalingHaveLocalAnswer ) {
// Send offer/answer through the signaling channel of our application
} else if (peerConnection.signalingState == RTCSignalingHaveRemoteOffer) {
// If we have a remote offer we should add it to the peer connection
[peerConnection createAnswerWithConstraints:constraints];
}
}
一旦你调用了setLocalDescription
,ICE engine 就会开始穿透,通过RTCPeerConnectionDelegate
的gotICECandidate
,这些都是本地的iceCandidates,必须发送到其他远端.
- (void)peerConnection:(RTCPeerConnection *)peerConnection gotICECandidate:(RTCICECandidate *)candidate { // Send candidate through the signaling channel }
当你收到一个ICE candidate时,你可以简便的直接连接远端.
如果一切正确执行或是你网络玩的还凑合,那么你现在应该收到addedSteam 代理方法, 在其中处理显示接收到的steam即可.
- (void)peerConnection:(RTCPeerConnection *)peerConnection addedStream:(RTCMediaStream *)stream
{
// Create a new render view with a size of your choice RTCEAGLVideoView *renderView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(100, 100)];
[stream.videoTracks.lastObject addRenderer:self.renderView];
// RTCEAGLVideoView is a subclass of UIView, so renderView
// can be inserted into your view hierarchy where it suits your application.
}