自我介绍
iOS应用工程师
感觉像我们在做iOS应用程序,Android应用程序和Web(java / ruby?? / php)一样,具有更广泛的支持。
动机
- 决定在企业中使用WebRTC实施视频通话。
- 但是,因为它使用了Twilio Voideo SDK(包括服务器的视频通话服务),所以即使我不太了解WebRTC,也可以按照示例和入门来实现它。
- 我想知道WebRTC本身是如何组成的。
什么是WebRTC?
维基百科的
- 这是一个免费的开源项目,可通过简单的API与Web浏览器和移动应用程序提供实时通信(RTC)。网页内的直接对等通信可在Web浏览器之间进行语音聊天,视频聊天和文件共享,而无需安装插件或下载本机应用程序。
- 大概是在2017年左右在技术上很受欢迎的印象
如何实现P2P通信
- 即使是P2P,它也不直接通信,而是通过服务器通信。
- SFU(选择性转发单元)=服务器代表客户端接收视频/音频并将其分发给客户端。
- MCU(多点控制单元)=通过在服务器端合成一种视频/音频来创建和分发。由于传送的视频是合成的,因此CPU负载很高。
当前设备和浏览器支持状态
- iOS:Safari 11(iOS 11)和更高版本受支持。 WKWebView尚不支持。 SFSafariViewController似乎可以在iOS 13上使用。
- Android:大约Android6,7(在android chrome中为android7)。由于不支持H264编解码器的视频,因此某些华为终端等可能无法很好地显示视频。
- 在PC浏览器上,Chrome,Firefox,Safari,Edge等大多数都可以工作。
WebRTC服务
- Skyweb,vonage,Twilio等将webrtc作为视频通话服务(包括服务器)提供给它,也有一个SDK。
-
但是这次,我想加深我的理解,同时探讨如何实现Google WebRTC(可能是官方库)。
- https://webrtc.github.io/webrtc-org/native-code/ios/
样品
-
这次,我将使用这个精彩的示例来看看实现。
- https://github.com/stasel/WebRTC-iOS
-
WebRTC正式具有使用Firebase的Web示例代码。
https://webrtc.org/getting-started/firebase-rtc-codelab
如何实现
连接
- 信令(ICE候选人(通信路线信息)和SDP之间的中继交换)通过服务器进行通信以建立通信。
- 在示例中,有一个nodejs服务器
- 在该示例中,使用了Google发布的STUN(用于NAT遍历的协议)服务器。
连接
- 创建三个通信线路,AudioTrack / VideoTrack /数据通道。
- 客户端之间交换要约SDP和应答SDP(称为信令)
- SDP包含可以使用的兼容视频/音频编解码器,流ID,端口号,IP地址等。
- SDP交换由WebSocket完成。
SDP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | v=0 o=- 7063950325015941208 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE 0 1 2 a=msid-semantic: WMS stream m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:Agzu a=ice-pwd:mef2jhxanVddiHDLNvw7BMl9 a=ice-options:trickle renomination a=fingerprint:sha-256 D7:E8:97:ED:96:EC:6D:8D:44:35:E8:51:A0:07:A0:EC:57:67:B9:76:16:31:1C:3E:6A:DF:A7:9D:E5:98:80:3A a=setup:actpass a=mid:0 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id a=sendrecv a=msid:stream audio0 a=rtcp-mux a=rtpmap:111 opus/48000/2 a=rtcp-fb:111 transport-cc a=fmtp:111 minptime=10;useinbandfec=1 a=rtpmap:103 ISAC/16000 a=rtpmap:104 ISAC/32000 a=rtpmap:9 G722/8000 a=rtpmap:102 ILBC/8000 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:106 CN/32000 a=rtpmap:105 CN/16000 a=rtpmap:13 CN/8000 a=rtpmap:110 telephone-event/48000 a=rtpmap:112 telephone-event/32000 a=rtpmap:113 telephone-event/16000 a=rtpmap:126 telephone-event/8000 a=ssrc:2493853505 cname:UcicltjXPgMMZ/9S a=ssrc:2493853505 msid:stream audio0 a=ssrc:2493853505 mslabel:stream a=ssrc:2493853505 label:audio0 m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 124 125 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:Agzu a=ice-pwd:mef2jhxanVddiHDLNvw7BMl9 a=ice-options:trickle renomination a=fingerprint:sha-256 D7:E8:97:ED:96:EC:6D:8D:44:35:E8:51:A0:07:A0:EC:57:67:B9:76:16:31:1C:3E:6A:DF:A7:9D:E5:98:80:3A a=setup:actpass a=mid:1 a=extmap:14 urn:ietf:params:rtp-hdrext:toffset a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time a=extmap:13 urn:3gpp:video-orientation a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing a=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07 a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id a=sendrecv a=msid:stream video0 a=rtcp-mux a=rtcp-rsize a=rtpmap:96 H264/90000 a=rtcp-fb:96 goog-remb a=rtcp-fb:96 transport-cc a=rtcp-fb:96 ccm fir a=rtcp-fb:96 nack a=rtcp-fb:96 nack pli a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c34 a=rtpmap:97 rtx/90000 a=fmtp:97 apt=96 a=rtpmap:98 H264/90000 a=rtcp-fb:98 goog-remb a=rtcp-fb:98 transport-cc a=rtcp-fb:98 ccm fir a=rtcp-fb:98 nack a=rtcp-fb:98 nack pli a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e034 a=rtpmap:99 rtx/90000 a=fmtp:99 apt=98 a=rtpmap:100 VP8/90000 a=rtcp-fb:100 goog-remb a=rtcp-fb:100 transport-cc a=rtcp-fb:100 ccm fir a=rtcp-fb:100 nack a=rtcp-fb:100 nack pli a=rtpmap:101 rtx/90000 a=fmtp:101 apt=100 a=rtpmap:127 red/90000 a=rtpmap:124 rtx/90000 a=fmtp:124 apt=127 a=rtpmap:125 ulpfec/90000 a=ssrc-group:FID 2153314113 822817334 a=ssrc:2153314113 cname:UcicltjXPgMMZ/9S a=ssrc:2153314113 msid:stream video0 a=ssrc:2153314113 mslabel:stream a=ssrc:2153314113 label:video0 a=ssrc:822817334 cname:UcicltjXPgMMZ/9S a=ssrc:822817334 msid:stream video0 a=ssrc:822817334 mslabel:stream a=ssrc:822817334 label:video0 m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=ice-ufrag:Agzu a=ice-pwd:mef2jhxanVddiHDLNvw7BMl9 a=ice-options:trickle renomination a=fingerprint:sha-256 D7:E8:97:ED:96:EC:6D:8D:44:35:E8:51:A0:07:A0:EC:57:67:B9:76:16:31:1C:3E:6A:DF:A7:9D:E5:98:80:3A a=setup:actpass a=mid:2 a=sctp-port:5000 a=max-message-size:262144 |
码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | let config = RTCConfiguration() config.iceServers = [RTCIceServer(urlStrings: iceServers)] // Unified plan is more superior than planB config.sdpSemantics = .unifiedPlan // gatherContinually will let WebRTC to listen to any network changes and send any new candidates to the other client config.continualGatheringPolicy = .gatherContinually let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: ["DtlsSrtpKeyAgreement":kRTCMediaConstraintsValueTrue]) self.peerConnection = WebRTCClient.factory.peerConnection(with: config, constraints: constraints, delegate: nil) super.init() self.createMediaSenders() self.configureAudioSession() self.peerConnection.delegate = self |
优惠SDP
- 发送的第一个SDP。
- 此时,可以传输视频编解码器:H264,VP8等。
1 2 3 4 5 6 7 8 9 10 11 12 13 | func offer(completion: @escaping (_ sdp: RTCSessionDescription) -> Void) { let constrains = RTCMediaConstraints(mandatoryConstraints: self.mediaConstrains, optionalConstraints: nil) self.peerConnection.offer(for: constrains) { (sdp, error) in guard let sdp = sdp else { return } self.peerConnection.setLocalDescription(sdp, completionHandler: { (error) in completion(sdp) }) } } |
回答SDP
- 接收并返回报价SDP的SDP。
- 它接收视频编解码器并支持VP8等发送。
1 2 3 4 5 6 7 8 9 10 11 12 13 | func answer(completion: @escaping (_ sdp: RTCSessionDescription) -> Void) { let constrains = RTCMediaConstraints(mandatoryConstraints: self.mediaConstrains, optionalConstraints: nil) self.peerConnection.answer(for: constrains) { (sdp, error) in guard let sdp = sdp else { return } self.peerConnection.setLocalDescription(sdp, completionHandler: { (error) in completion(sdp) }) } } |
视频捕获和传输开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | func startCaptureLocalVideo(renderer: RTCVideoRenderer) { // 接続時に準備したカメラの映像を取得して映像フレームを生成するインスタンス guard let capturer = self.videoCapturer as? RTCCameraVideoCapturer else { return } // ビデオキャプチャ対応しているデバイス(フロントカメラ)を取得 guard let frontCamera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == .front }), // フロントカメラの最大解像度を取得 let format = (RTCCameraVideoCapturer.supportedFormats(for: frontCamera).sorted { (f1, f2) -> Bool in let width1 = CMVideoFormatDescriptionGetDimensions(f1.formatDescription).width let width2 = CMVideoFormatDescriptionGetDimensions(f2.formatDescription).width return width1 < width2 }).last, // 最大フレームレートを取得 let fps = (format.videoSupportedFrameRateRanges.sorted { return $0.maxFrameRate < $1.maxFrameRate }.last) else { return } // フロントカメラのキャプチャ開始、相手への送信開始 capturer.startCapture(with: frontCamera, format: format, fps: Int(fps.maxFrameRate)) // 自分の映像を描画 // localVideoTrackは接続時に準備しているローカル側の映像トラックなので、localVideoTrackに入ってくるデータをrendererに流し込むための設定 self.localVideoTrack?.add(renderer) } |
视频渲染集
1 2 3 4 5 | func renderRemoteVideo(to renderer: RTCVideoRenderer) { // 相手の映像を描画 // remoteVideoTrackは接続時に準備している相手側からの受信映像トラックなので、remoteVideoTrackに入ってくるデータをrendererに流し込むための設定 self.remoteVideoTrack?.add(renderer) } |
当您想在视频上显示图像
当您想在视频上显示图像
有以下方法。
- 一种将您发送的视频帧转换为图像的模式
- 使用DataChannel的模式
模式
,使您发送的视频帧成为图像
- 基本上,实现了GoogleWebRTC,以便将视频从前置/后置摄像头发送到另一方。
- 但是,由于有一个委托(RTCVideoCapturerDelegate)用于挂钩要在每一帧发送给另一方的视频,因此请使用它。
- 这时,使用RTCVideoSource#capturer()传递RTCVideoFrame。
- 稍后将描述实现。这不是示例,而是原始实现。
使用DataChannel的模式
- RTCDataBuffer(处理二进制数据的类)可以在数据通道中发送和接收,因此请使用它。
- 发送此数据后,图像将显示在发送方和接收方,并确定并处理规格。
使您发送的视频帧成为图像
原来是这样..
1 2 | self.videoSource = WebRTCClient.factory.videoSource() self.videoCapturer = RTCCameraVideoCapturer(delegate: self.videoSource) |
使它看起来像这样。
1 2 3 4 5 6 7 8 9 10 11 12 | self.videoSource = WebRTCClient.factory.videoSource() //self.videoCapturer = RTCCameraVideoCapturer(delegate: self.videoSource) self.videoCapturer = RTCCameraVideoCapturer(delegate: self) //selfはRTCVideoCapturerDelegateを継承 // WebRTCClient WebRTCの処理を行うためのクラス extension WebRTCClient: RTCVideoCapturerDelegate { // これが映像フレーム送信前にコールバックされる。 func capturer(_ capturer: RTCVideoCapturer, didCapture frame: RTCVideoFrame) { self.captureVideoFrameChannel(videoSource: self.videoSource, videoCapturer: capturer, srcframe: frame) } } |
从图像创建框架并将其发送
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | private func captureVideoFrameChannel(videoSource: RTCVideoSource, videoCapturer: RTCVideoCapturer) { let image = UIImage(named: "gundam")! func cvPixelBuffer(image: UIImage) -> CVPixelBuffer? { let width = image.cgImage!.width let height = image.cgImage!.height let options: [NSObject: Any] = [ kCVPixelBufferCGImageCompatibilityKey: true, kCVPixelBufferCGBitmapContextCompatibilityKey: true, ] var pxbufferTemp: CVPixelBuffer? = nil let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32ARGB, options as CFDictionary, &pxbufferTemp); guard let pxbuffer = pxbufferTemp, status == kCVReturnSuccess else { fatalError() } CVPixelBufferLockBaseAddress(pxbuffer, []) let pxdataTmp = CVPixelBufferGetBaseAddress(pxbuffer) guard let pxdata = pxdataTmp else { fatalError() } let rgbColorSpace = CGColorSpaceCreateDeviceRGB(); guard let context = CGContext(data: pxdata, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 4 * width, space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) else { fatalError() } context.draw(image.cgImage!, in: CGRect(origin: .zero, size: CGSize.init(width: width, height: height))) CVPixelBufferUnlockBaseAddress(pxbuffer, CVPixelBufferLockFlags(rawValue: 0)) return pxbuffer } func cmSampleBuffer(image: UIImage) -> CMSampleBuffer { let pixelBuffer = cvPixelBuffer(image: image) var newSampleBuffer: CMSampleBuffer? = nil var timimgInfo: CMSampleTimingInfo = CMSampleTimingInfo.invalid var videoInfo: CMVideoFormatDescription? = nil CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer!, formatDescriptionOut: &videoInfo) CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer!, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: videoInfo!, sampleTiming: &timimgInfo, sampleBufferOut: &newSampleBuffer) return newSampleBuffer! } let pixelBuffer = CMSampleBufferGetImageBuffer(cmSampleBuffer(image: image))! let rtcpixelBuffer = RTCCVPixelBuffer(pixelBuffer: pixelBuffer) // これが映像フレームのデータ let videoFrame = RTCVideoFrame( buffer: rtcpixelBuffer, rotation: RTCVideoRotation._0, timeStampNs: Int64(Date().timeIntervalSince1970 * 1_000_000_000) ) // ここで映像データ送信! videoSource.capturer(videoCapturer, didCapture: videoFrame) } |
- 当发送视频帧时,发送方还会发送时间戳,接收方会根据时间戳(可能)进行渲染。
- 因此,如果RTC VideoFrame时间戳不正确,则不会在接收端显示。
终于
- 也许还没有针对iOS应用的易于使用的客户端库(可能)
- 有包括服务器在内的各种服务,因此相对易于使用。
- 我想看音频实现更多..
这次使用的源
https://github.com/HikaruSato/WebRTC-iOS/tree/tutorial
参考(谢谢!)
- https://gist.github.com/szktty/999a34c64cc4ea60de43c4c1adc93203
- https://gist.github.com/voluntas/67e5a26915751226fdcf
- https://qiita.com/mush/items/121e45fefed009b6ad5e