session-ios/Signal/src/call/PeerConnectionClient.swift

1293 lines
52 KiB
Swift

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
import WebRTC
import SignalServiceKit
import SignalMessaging
// HACK - Seeing crazy SEGFAULTs on iOS9 when accessing these objc externs.
// iOS10 seems unaffected. Reproducible for ~1 in 3 calls.
// Binding them to a file constant seems to work around the problem.
let kAudioTrackType = kRTCMediaStreamTrackKindAudio
let kVideoTrackType = kRTCMediaStreamTrackKindVideo
let kMediaConstraintsMinWidth = kRTCMediaConstraintsMinWidth
let kMediaConstraintsMaxWidth = kRTCMediaConstraintsMaxWidth
let kMediaConstraintsMinHeight = kRTCMediaConstraintsMinHeight
let kMediaConstraintsMaxHeight = kRTCMediaConstraintsMaxHeight
/**
* The PeerConnectionClient notifies it's delegate (the CallService) of key events in the call signaling life cycle
*
* The delegate's methods will always be called on the main thread.
*/
protocol PeerConnectionClientDelegate: class {
/**
* The connection has been established. The clients can now communicate.
* This can be called multiple times throughout the call in the event of temporary network disconnects.
*/
func peerConnectionClientIceConnected(_ peerconnectionClient: PeerConnectionClient)
/**
* The connection failed to establish. The clients will not be able to communicate.
*/
func peerConnectionClientIceFailed(_ peerconnectionClient: PeerConnectionClient)
/**
* After initially connecting, the connection disconnected.
* It maybe be temporary, in which case `peerConnectionClientIceConnected` will be called again once we're reconnected.
* Otherwise, `peerConnectionClientIceFailed` will eventually called.
*/
func peerConnectionClientIceDisconnected(_ peerconnectionClient: PeerConnectionClient)
/**
* During the Signaling process each client generates IceCandidates locally, which contain information about how to
* reach the local client via the internet. The delegate must shuttle these IceCandates to the other (remote) client
* out of band, as part of establishing a connection over WebRTC.
*/
func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate)
/**
* Once the peerconnection is established, we can receive messages via the data channel, and notify the delegate.
*/
func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, received dataChannelMessage: WebRTCProtoData)
/**
* Fired whenever the local video track become active or inactive.
*/
func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, didUpdateLocalVideoCaptureSession captureSession: AVCaptureSession?)
/**
* Fired whenever the remote video track become active or inactive.
*/
func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, didUpdateRemoteVideoTrack videoTrack: RTCVideoTrack?)
}
// In Swift (at least in Swift v3.3), weak variables aren't thread safe. It
// isn't safe to resolve/acquire/lock a weak reference into a strong reference
// while the instance might be being deallocated on another thread.
//
// PeerConnectionProxy provides thread-safe access to a strong reference.
// PeerConnectionClient has an PeerConnectionProxy to itself that its many async blocks
// (which run on more than one thread) can use to safely try to acquire a strong
// reference to the PeerConnectionClient. In ARC we'd normally, we'd avoid
// having an instance retain a strong reference to itself to avoid retain
// cycles, but it's safe in this case: PeerConnectionClient is owned (and only
// used by) a single entity CallService and CallService always calls
// [PeerConnectionClient terminate] when it is done with a PeerConnectionClient
// instance, so terminate is a reliable place where we can break the retain cycle.
//
// Note that we use the proxy in two ways:
//
// * As a delegate for the peer connection and the data channel,
// safely forwarding delegate method invocations to the PCC.
// * To safely obtain references to the PCC within the PCC's
// async blocks.
//
// This should be fixed in Swift 4, but it isn't.
//
// To test using the following scenarios:
//
// * Alice and Bob place simultaneous calls to each other. Both should get busy.
// Repeat 10-20x. Then verify that they can connect a call by having just one
// call the other.
// * Alice or Bob (randomly alternating) calls the other. Recipient (randomly)
// accepts call or hangs up. If accepted, Alice or Bob (randomly) hangs up.
// Repeat immediately, as fast as you can, 10-20x.
class PeerConnectionProxy: NSObject, RTCPeerConnectionDelegate, RTCDataChannelDelegate {
private var value: PeerConnectionClient?
deinit {
Logger.info("[PeerConnectionProxy] deinit")
}
func set(value: PeerConnectionClient) {
objc_sync_enter(self)
self.value = value
objc_sync_exit(self)
}
func get() -> PeerConnectionClient? {
objc_sync_enter(self)
let result = value
objc_sync_exit(self)
if result == nil {
// Every time this method returns nil is a
// possible crash avoided.
Logger.verbose("\(logTag) cleared get.")
}
return result
}
func clear() {
Logger.info("\(logTag) \(#function)")
objc_sync_enter(self)
value = nil
objc_sync_exit(self)
}
// MARK: - RTCPeerConnectionDelegate
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
self.get()?.peerConnection(peerConnection, didChange: stateChanged)
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
self.get()?.peerConnection(peerConnection, didAdd: stream)
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
self.get()?.peerConnection(peerConnection, didRemove: stream)
}
public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
self.get()?.peerConnectionShouldNegotiate(peerConnection)
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
self.get()?.peerConnection(peerConnection, didChange: newState)
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
self.get()?.peerConnection(peerConnection, didChange: newState)
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
self.get()?.peerConnection(peerConnection, didGenerate: candidate)
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
self.get()?.peerConnection(peerConnection, didRemove: candidates)
}
public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
self.get()?.peerConnection(peerConnection, didOpen: dataChannel)
}
// MARK: - RTCDataChannelDelegate
public func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) {
self.get()?.dataChannelDidChangeState(dataChannel)
}
public func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) {
self.get()?.dataChannel(dataChannel, didReceiveMessageWith: buffer)
}
public func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) {
self.get()?.dataChannel(dataChannel, didChangeBufferedAmount: amount)
}
}
/**
* `PeerConnectionClient` is our interface to WebRTC.
*
* It is primarily a wrapper around `RTCPeerConnection`, which is responsible for sending and receiving our call data
* including audio, video, and some post-connected signaling (hangup, add video)
*/
class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelDelegate, VideoCaptureSettingsDelegate {
enum Identifiers: String {
case mediaStream = "ARDAMS",
videoTrack = "ARDAMSv0",
audioTrack = "ARDAMSa0",
dataChannelSignaling = "signaling"
}
// A state in this class should only be accessed on this queue in order to
// serialize access.
//
// This queue is also used to perform expensive calls to the WebRTC API.
private static let signalingQueue = DispatchQueue(label: "CallServiceSignalingQueue")
// Delegate is notified of key events in the call lifecycle.
//
// This property should only be accessed on the main thread.
private weak var delegate: PeerConnectionClientDelegate?
// Connection
private var peerConnection: RTCPeerConnection?
private let iceServers: [RTCIceServer]
private let connectionConstraints: RTCMediaConstraints
private let configuration: RTCConfiguration
private let factory = RTCPeerConnectionFactory()
// DataChannel
private var dataChannel: RTCDataChannel?
// Audio
private var audioSender: RTCRtpSender?
private var audioTrack: RTCAudioTrack?
private var audioConstraints: RTCMediaConstraints
// Video
private var videoCaptureController: VideoCaptureController?
private var videoSender: RTCRtpSender?
// RTCVideoTrack is fragile and prone to throwing exceptions and/or
// causing deadlock in its destructor. Therefore we take great care
// with this property.
private var localVideoTrack: RTCVideoTrack?
private var remoteVideoTrack: RTCVideoTrack?
private var cameraConstraints: RTCMediaConstraints
private let proxy = PeerConnectionProxy()
// Note that we're deliberately leaking proxy instances using this
// collection to avoid EXC_BAD_ACCESS. Calls are rare and the proxy
// is tiny (a single property), so it's better to leak and be safe.
private static var expiredProxies = [PeerConnectionProxy]()
init(iceServers: [RTCIceServer], delegate: PeerConnectionClientDelegate, callDirection: CallDirection, useTurnOnly: Bool) {
SwiftAssertIsOnMainThread(#function)
self.iceServers = iceServers
self.delegate = delegate
configuration = RTCConfiguration()
configuration.iceServers = iceServers
configuration.bundlePolicy = .maxBundle
configuration.rtcpMuxPolicy = .require
if useTurnOnly {
Logger.debug("\(PeerConnectionClient.logTag) using iceTransportPolicy: relay")
configuration.iceTransportPolicy = .relay
} else {
Logger.debug("\(PeerConnectionClient.logTag) using iceTransportPolicy: default")
}
let connectionConstraintsDict = ["DtlsSrtpKeyAgreement": "true"]
connectionConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: connectionConstraintsDict)
audioConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
cameraConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
super.init()
proxy.set(value: self)
peerConnection = factory.peerConnection(with: configuration,
constraints: connectionConstraints,
delegate: proxy)
createAudioSender()
createVideoSender()
if callDirection == .outgoing {
// When placing an outgoing call, it's our responsibility to create the DataChannel.
// Recipient will not have to do this explicitly.
createSignalingDataChannel()
}
}
deinit {
// TODO: We can demote this log level to debug once we're confident that
// this class is always deallocated.
Logger.info("[PeerConnectionClient] deinit")
}
// MARK: - Media Streams
private func createSignalingDataChannel() {
SwiftAssertIsOnMainThread(#function)
guard let peerConnection = peerConnection else {
Logger.debug("\(logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
let configuration = RTCDataChannelConfiguration()
// Insist upon an "ordered" TCP data channel for delivery reliability.
configuration.isOrdered = true
guard let dataChannel = peerConnection.dataChannel(forLabel: Identifiers.dataChannelSignaling.rawValue,
configuration: configuration) else {
// TODO fail outgoing call?
owsFail("dataChannel was unexpectedly nil")
return
}
dataChannel.delegate = proxy
assert(self.dataChannel == nil)
self.dataChannel = dataChannel
}
// MARK: - Video
fileprivate func createVideoSender() {
SwiftAssertIsOnMainThread(#function)
Logger.debug("\(logTag) in \(#function)")
assert(self.videoSender == nil, "\(#function) should only be called once.")
guard !Platform.isSimulator else {
Logger.warn("\(logTag) Refusing to create local video track on simulator which has no capture device.")
return
}
guard let peerConnection = peerConnection else {
Logger.debug("\(logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
let videoSource = factory.videoSource()
let localVideoTrack = factory.videoTrack(with: videoSource, trackId: Identifiers.videoTrack.rawValue)
self.localVideoTrack = localVideoTrack
// Disable by default until call is connected.
// FIXME - do we require mic permissions at this point?
// if so maybe it would be better to not even add the track until the call is connected
// instead of creating it and disabling it.
localVideoTrack.isEnabled = false
let capturer = RTCCameraVideoCapturer(delegate: videoSource)
self.videoCaptureController = VideoCaptureController(capturer: capturer, settingsDelegate: self)
let videoSender = peerConnection.sender(withKind: kVideoTrackType, streamId: Identifiers.mediaStream.rawValue)
videoSender.track = localVideoTrack
self.videoSender = videoSender
}
public func setCameraSource(isUsingFrontCamera: Bool) {
SwiftAssertIsOnMainThread(#function)
let proxyCopy = self.proxy
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard let captureController = strongSelf.videoCaptureController else {
owsFail("\(self.logTag) in \(#function) captureController was unexpectedly nil")
return
}
captureController.switchCamera(isUsingFrontCamera: isUsingFrontCamera)
}
}
public func setLocalVideoEnabled(enabled: Bool) {
SwiftAssertIsOnMainThread(#function)
let proxyCopy = self.proxy
let completion = {
guard let strongSelf = proxyCopy.get() else { return }
guard let strongDelegate = strongSelf.delegate else { return }
let captureSession: AVCaptureSession? = {
guard enabled else {
return nil
}
guard let captureController = strongSelf.videoCaptureController else {
owsFail("\(self.logTag) in \(#function) videoCaptureController was unexpectedly nil")
return nil
}
return captureController.captureSession
}()
strongDelegate.peerConnectionClient(strongSelf, didUpdateLocalVideoCaptureSession: captureSession)
}
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard strongSelf.peerConnection != nil else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
guard let videoCaptureController = strongSelf.videoCaptureController else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
guard let localVideoTrack = strongSelf.localVideoTrack else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
localVideoTrack.isEnabled = enabled
if enabled {
Logger.debug("\(strongSelf.logTag) in \(#function) starting video capture")
videoCaptureController.startCapture()
} else {
Logger.debug("\(strongSelf.logTag) in \(#function) stopping video capture")
videoCaptureController.stopCapture()
}
DispatchQueue.main.async(execute: completion)
}
}
// MARK: VideoCaptureSettingsDelegate
var videoWidth: Int32 {
return 400
}
var videoHeight: Int32 {
return 400
}
// MARK: - Audio
fileprivate func createAudioSender() {
SwiftAssertIsOnMainThread(#function)
Logger.debug("\(logTag) in \(#function)")
assert(self.audioSender == nil, "\(#function) should only be called once.")
guard let peerConnection = peerConnection else {
Logger.debug("\(logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
let audioSource = factory.audioSource(with: self.audioConstraints)
let audioTrack = factory.audioTrack(with: audioSource, trackId: Identifiers.audioTrack.rawValue)
self.audioTrack = audioTrack
// Disable by default until call is connected.
// FIXME - do we require mic permissions at this point?
// if so maybe it would be better to not even add the track until the call is connected
// instead of creating it and disabling it.
audioTrack.isEnabled = false
let audioSender = peerConnection.sender(withKind: kAudioTrackType, streamId: Identifiers.mediaStream.rawValue)
audioSender.track = audioTrack
self.audioSender = audioSender
}
public func setAudioEnabled(enabled: Bool) {
SwiftAssertIsOnMainThread(#function)
let proxyCopy = self.proxy
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard strongSelf.peerConnection != nil else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
guard let audioTrack = strongSelf.audioTrack else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
audioTrack.isEnabled = enabled
}
}
// MARK: - Session negotiation
private var defaultOfferConstraints: RTCMediaConstraints {
let mandatoryConstraints = [
"OfferToReceiveAudio": "true",
"OfferToReceiveVideo": "true"
]
return RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: nil)
}
public func createOffer() -> Promise<HardenedRTCSessionDescription> {
SwiftAssertIsOnMainThread(#function)
let proxyCopy = self.proxy
let (promise, fulfill, reject) = Promise<HardenedRTCSessionDescription>.pending()
let completion: ((RTCSessionDescription?, Error?) -> Void) = { (sdp, error) in
guard let strongSelf = proxyCopy.get() else {
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
strongSelf.assertOnSignalingQueue()
guard strongSelf.peerConnection != nil else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
if let error = error {
reject(error)
return
}
guard let sessionDescription = sdp else {
Logger.error("\(strongSelf.logTag) No session description was obtained, even though there was no error reported.")
let error = OWSErrorMakeUnableToProcessServerResponseError()
reject(error)
return
}
fulfill(HardenedRTCSessionDescription(rtcSessionDescription: sessionDescription))
}
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else {
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
strongSelf.assertOnSignalingQueue()
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
peerConnection.offer(for: strongSelf.defaultOfferConstraints, completionHandler: { (sdp: RTCSessionDescription?, error: Error?) in
PeerConnectionClient.signalingQueue.async {
completion(sdp, error)
}
})
}
return promise
}
public func setLocalSessionDescriptionInternal(_ sessionDescription: HardenedRTCSessionDescription) -> Promise<Void> {
let proxyCopy = self.proxy
let (promise, fulfill, reject) = Promise<Void>.pending()
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else {
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
strongSelf.assertOnSignalingQueue()
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
Logger.verbose("\(strongSelf.logTag) setting local session description: \(sessionDescription)")
peerConnection.setLocalDescription(sessionDescription.rtcSessionDescription, completionHandler: { (error) in
if let error = error {
reject(error)
} else {
fulfill(())
}
})
}
return promise
}
public func setLocalSessionDescription(_ sessionDescription: HardenedRTCSessionDescription) -> Promise<Void> {
SwiftAssertIsOnMainThread(#function)
let proxyCopy = self.proxy
let (promise, fulfill, reject) = Promise<Void>.pending()
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else {
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
strongSelf.assertOnSignalingQueue()
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
Logger.verbose("\(strongSelf.logTag) setting local session description: \(sessionDescription)")
peerConnection.setLocalDescription(sessionDescription.rtcSessionDescription,
completionHandler: { error in
if let error = error {
reject(error)
return
}
fulfill(())
})
}
return promise
}
public func negotiateSessionDescription(remoteDescription: RTCSessionDescription, constraints: RTCMediaConstraints) -> Promise<HardenedRTCSessionDescription> {
SwiftAssertIsOnMainThread(#function)
let proxyCopy = self.proxy
return setRemoteSessionDescription(remoteDescription)
.then(on: PeerConnectionClient.signalingQueue) {
guard let strongSelf = proxyCopy.get() else {
return Promise(error: NSError(domain: "Obsolete client", code: 0, userInfo: nil))
}
return strongSelf.negotiateAnswerSessionDescription(constraints: constraints)
}
}
public func setRemoteSessionDescription(_ sessionDescription: RTCSessionDescription) -> Promise<Void> {
SwiftAssertIsOnMainThread(#function)
let proxyCopy = self.proxy
let (promise, fulfill, reject) = Promise<Void>.pending()
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else {
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
strongSelf.assertOnSignalingQueue()
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
Logger.verbose("\(strongSelf.logTag) setting remote description: \(sessionDescription)")
peerConnection.setRemoteDescription(sessionDescription,
completionHandler: { error in
if let error = error {
reject(error)
return
}
fulfill(())
})
}
return promise
}
private func negotiateAnswerSessionDescription(constraints: RTCMediaConstraints) -> Promise<HardenedRTCSessionDescription> {
assertOnSignalingQueue()
let proxyCopy = self.proxy
let (promise, fulfill, reject) = Promise<HardenedRTCSessionDescription>.pending()
let completion: ((RTCSessionDescription?, Error?) -> Void) = { (sdp, error) in
guard let strongSelf = proxyCopy.get() else {
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
strongSelf.assertOnSignalingQueue()
guard strongSelf.peerConnection != nil else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
if let error = error {
reject(error)
return
}
guard let sessionDescription = sdp else {
Logger.error("\(strongSelf.logTag) unexpected empty session description, even though no error was reported.")
let error = OWSErrorMakeUnableToProcessServerResponseError()
reject(error)
return
}
let hardenedSessionDescription = HardenedRTCSessionDescription(rtcSessionDescription: sessionDescription)
strongSelf.setLocalSessionDescriptionInternal(hardenedSessionDescription)
.then(on: PeerConnectionClient.signalingQueue) { _ in
fulfill(hardenedSessionDescription)
}.catch { error in
reject(error)
}
}
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else {
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
strongSelf.assertOnSignalingQueue()
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil))
return
}
Logger.debug("\(strongSelf.logTag) negotiating answer session.")
peerConnection.answer(for: constraints, completionHandler: { (sdp: RTCSessionDescription?, error: Error?) in
PeerConnectionClient.signalingQueue.async {
completion(sdp, error)
}
})
}
return promise
}
public func addRemoteIceCandidate(_ candidate: RTCIceCandidate) {
let proxyCopy = self.proxy
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
Logger.info("\(strongSelf.logTag) adding remote ICE candidate: \(candidate.sdp)")
peerConnection.add(candidate)
}
}
public func terminate() {
SwiftAssertIsOnMainThread(#function)
Logger.debug("\(logTag) in \(#function)")
// Clear the delegate immediately so that we can guarantee that
// no delegate methods are called after terminate() returns.
delegate = nil
// Clear the proxy immediately so that enqueued work is aborted
// going forward.
PeerConnectionClient.expiredProxies.append(proxy)
proxy.clear()
// Don't use [weak self]; we always want to perform terminateInternal().
PeerConnectionClient.signalingQueue.async {
self.terminateInternal()
}
}
private func terminateInternal() {
assertOnSignalingQueue()
Logger.debug("\(logTag) in \(#function)")
// Some notes on preventing crashes while disposing of peerConnection for video calls
// from: https://groups.google.com/forum/#!searchin/discuss-webrtc/objc$20crash$20dealloc%7Csort:relevance/discuss-webrtc/7D-vk5yLjn8/rBW2D6EW4GYJ
// The sequence to make it work appears to be
//
// [capturer stop]; // I had to add this as a method to RTCVideoCapturer
// [localRenderer stop];
// [remoteRenderer stop];
// [peerConnection close];
// audioTrack is a strong property because we need access to it to mute/unmute, but I was seeing it
// become nil when it was only a weak property. So we retain it and manually nil the reference here, because
// we are likely to crash if we retain any peer connection properties when the peerconnection is released
localVideoTrack?.isEnabled = false
remoteVideoTrack?.isEnabled = false
if let dataChannel = self.dataChannel {
dataChannel.delegate = nil
}
dataChannel = nil
audioSender = nil
audioTrack = nil
videoSender = nil
localVideoTrack = nil
remoteVideoTrack = nil
videoCaptureController = nil
if let peerConnection = peerConnection {
peerConnection.delegate = nil
peerConnection.close()
}
peerConnection = nil
}
// MARK: - Data Channel
// should only be accessed on PeerConnectionClient.signalingQueue
var pendingDataChannelMessages: [PendingDataChannelMessage] = []
struct PendingDataChannelMessage {
let data: Data
let description: String
let isCritical: Bool
}
public func sendDataChannelMessage(data: Data, description: String, isCritical: Bool) {
SwiftAssertIsOnMainThread(#function)
let proxyCopy = self.proxy
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard strongSelf.peerConnection != nil else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client: \(description)")
return
}
guard let dataChannel = strongSelf.dataChannel else {
if isCritical {
Logger.info("\(strongSelf.logTag) in \(#function) enqueuing critical data channel message for after we have a dataChannel: \(description)")
strongSelf.pendingDataChannelMessages.append(PendingDataChannelMessage(data: data, description: description, isCritical: isCritical))
} else {
Logger.error("\(strongSelf.logTag) in \(#function) ignoring sending \(data) for nil dataChannel: \(description)")
}
return
}
Logger.debug("\(strongSelf.logTag) sendDataChannelMessage trying: \(description)")
let buffer = RTCDataBuffer(data: data, isBinary: false)
let result = dataChannel.sendData(buffer)
if result {
Logger.debug("\(strongSelf.logTag) sendDataChannelMessage succeeded: \(description)")
} else {
Logger.warn("\(strongSelf.logTag) sendDataChannelMessage failed: \(description)")
if isCritical {
OWSProdError(OWSAnalyticsEvents.peerConnectionClientErrorSendDataChannelMessageFailed(), file: #file, function: #function, line: #line)
}
}
}
}
// MARK: RTCDataChannelDelegate
/** The data channel state changed. */
internal func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) {
Logger.debug("\(logTag) dataChannelDidChangeState: \(dataChannel)")
}
/** The data channel successfully received a data buffer. */
internal func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) {
let proxyCopy = self.proxy
let completion: (WebRTCProtoData) -> Void = { (dataChannelMessage) in
SwiftAssertIsOnMainThread(#function)
guard let strongSelf = proxyCopy.get() else { return }
guard let strongDelegate = strongSelf.delegate else { return }
strongDelegate.peerConnectionClient(strongSelf, received: dataChannelMessage)
}
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard strongSelf.peerConnection != nil else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
Logger.debug("\(strongSelf.logTag) dataChannel didReceiveMessageWith buffer:\(buffer)")
var dataChannelMessage: WebRTCProtoData
do {
dataChannelMessage = try WebRTCProtoData.parseData(buffer.data)
} catch {
Logger.error("\(strongSelf.logTag) failed to parse dataProto")
return
}
DispatchQueue.main.async {
completion(dataChannelMessage)
}
}
}
/** The data channel's |bufferedAmount| changed. */
internal func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) {
Logger.debug("\(logTag) didChangeBufferedAmount: \(amount)")
}
// MARK: - RTCPeerConnectionDelegate
/** Called when the SignalingState changed. */
internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
Logger.debug("\(logTag) didChange signalingState:\(stateChanged.debugDescription)")
}
/** Called when media is received on a new stream from remote peer. */
internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didAdd stream: RTCMediaStream) {
let proxyCopy = self.proxy
let completion: (RTCVideoTrack) -> Void = { (remoteVideoTrack) in
SwiftAssertIsOnMainThread(#function)
guard let strongSelf = proxyCopy.get() else { return }
guard let strongDelegate = strongSelf.delegate else { return }
// TODO: Consider checking for termination here.
strongDelegate.peerConnectionClient(strongSelf, didUpdateRemoteVideoTrack: remoteVideoTrack)
}
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
guard peerConnection == peerConnectionParam else {
owsFail("\(strongSelf.logTag) in \(#function) mismatched peerConnection callback.")
return
}
guard stream.videoTracks.count > 0 else {
owsFail("\(strongSelf.logTag) in \(#function) didAdd stream missing stream.")
return
}
let remoteVideoTrack = stream.videoTracks[0]
Logger.debug("\(strongSelf.logTag) didAdd stream:\(stream) video tracks: \(stream.videoTracks.count) audio tracks: \(stream.audioTracks.count)")
strongSelf.remoteVideoTrack = remoteVideoTrack
DispatchQueue.main.async {
completion(remoteVideoTrack)
}
}
}
/** Called when a remote peer closes a stream. */
internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didRemove stream: RTCMediaStream) {
Logger.debug("\(logTag) didRemove Stream:\(stream)")
}
/** Called when negotiation is needed, for example ICE has restarted. */
internal func peerConnectionShouldNegotiate(_ peerConnectionParam: RTCPeerConnection) {
Logger.debug("\(logTag) shouldNegotiate")
}
/** Called any time the IceConnectionState changes. */
internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
let proxyCopy = self.proxy
let connectedCompletion : () -> Void = {
SwiftAssertIsOnMainThread(#function)
guard let strongSelf = proxyCopy.get() else { return }
guard let strongDelegate = strongSelf.delegate else { return }
strongDelegate.peerConnectionClientIceConnected(strongSelf)
}
let failedCompletion : () -> Void = {
SwiftAssertIsOnMainThread(#function)
guard let strongSelf = proxyCopy.get() else { return }
guard let strongDelegate = strongSelf.delegate else { return }
strongDelegate.peerConnectionClientIceFailed(strongSelf)
}
let disconnectedCompletion : () -> Void = {
SwiftAssertIsOnMainThread(#function)
guard let strongSelf = proxyCopy.get() else { return }
guard let strongDelegate = strongSelf.delegate else { return }
strongDelegate.peerConnectionClientIceDisconnected(strongSelf)
}
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
guard peerConnection == peerConnectionParam else {
owsFail("\(strongSelf.logTag) in \(#function) mismatched peerConnection callback.")
return
}
Logger.info("\(strongSelf.logTag) didChange IceConnectionState:\(newState.debugDescription)")
switch newState {
case .connected, .completed:
DispatchQueue.main.async(execute: connectedCompletion)
case .failed:
Logger.warn("\(strongSelf.logTag) RTCIceConnection failed.")
DispatchQueue.main.async(execute: failedCompletion)
case .disconnected:
Logger.warn("\(strongSelf.logTag) RTCIceConnection disconnected.")
DispatchQueue.main.async(execute: disconnectedCompletion)
default:
Logger.debug("\(strongSelf.logTag) ignoring change IceConnectionState:\(newState.debugDescription)")
}
}
}
/** Called any time the IceGatheringState changes. */
internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
Logger.info("\(logTag) didChange IceGatheringState:\(newState.debugDescription)")
}
/** New ice candidate has been found. */
internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
let proxyCopy = self.proxy
let completion: (RTCIceCandidate) -> Void = { (candidate) in
SwiftAssertIsOnMainThread(#function)
guard let strongSelf = proxyCopy.get() else { return }
guard let strongDelegate = strongSelf.delegate else { return }
strongDelegate.peerConnectionClient(strongSelf, addedLocalIceCandidate: candidate)
}
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
guard peerConnection == peerConnectionParam else {
owsFail("\(strongSelf.logTag) in \(#function) mismatched peerConnection callback.")
return
}
Logger.info("\(strongSelf.logTag) adding local ICE candidate:\(candidate.sdp)")
DispatchQueue.main.async {
completion(candidate)
}
}
}
/** Called when a group of local Ice candidates have been removed. */
internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
Logger.debug("\(logTag) didRemove IceCandidates:\(candidates)")
}
/** New data channel has been opened. */
internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
let proxyCopy = self.proxy
let completion: ([PendingDataChannelMessage]) -> Void = { (pendingMessages) in
SwiftAssertIsOnMainThread(#function)
guard let strongSelf = proxyCopy.get() else { return }
pendingMessages.forEach { message in
strongSelf.sendDataChannelMessage(data: message.data, description: message.description, isCritical: message.isCritical)
}
}
PeerConnectionClient.signalingQueue.async {
guard let strongSelf = proxyCopy.get() else { return }
guard let peerConnection = strongSelf.peerConnection else {
Logger.debug("\(strongSelf.logTag) \(#function) Ignoring obsolete event in terminated client")
return
}
guard peerConnection == peerConnectionParam else {
owsFail("\(strongSelf.logTag) in \(#function) mismatched peerConnection callback.")
return
}
Logger.info("\(strongSelf.logTag) didOpen dataChannel:\(dataChannel)")
if strongSelf.dataChannel != nil {
owsFail("\(strongSelf.logTag) in \(#function) dataChannel unexpectedly set twice.")
}
strongSelf.dataChannel = dataChannel
dataChannel.delegate = strongSelf.proxy
let pendingMessages = strongSelf.pendingDataChannelMessages
strongSelf.pendingDataChannelMessages = []
DispatchQueue.main.async {
completion(pendingMessages)
}
}
}
// MARK: Helpers
/**
* We synchronize access to state in this class using this queue.
*/
private func assertOnSignalingQueue() {
assertOnQueue(type(of: self).signalingQueue)
}
// MARK: Test-only accessors
internal func peerConnectionForTests() -> RTCPeerConnection {
SwiftAssertIsOnMainThread(#function)
var result: RTCPeerConnection? = nil
PeerConnectionClient.signalingQueue.sync {
result = peerConnection
Logger.info("\(self.logTag) called \(#function)")
}
return result!
}
internal func dataChannelForTests() -> RTCDataChannel {
SwiftAssertIsOnMainThread(#function)
var result: RTCDataChannel? = nil
PeerConnectionClient.signalingQueue.sync {
result = dataChannel
Logger.info("\(self.logTag) called \(#function)")
}
return result!
}
internal func flushSignalingQueueForTests() {
SwiftAssertIsOnMainThread(#function)
PeerConnectionClient.signalingQueue.sync {
// Noop.
}
}
}
/**
* Restrict an RTCSessionDescription to more secure parameters
*/
class HardenedRTCSessionDescription {
let rtcSessionDescription: RTCSessionDescription
var sdp: String { return rtcSessionDescription.sdp }
init(rtcSessionDescription: RTCSessionDescription) {
self.rtcSessionDescription = HardenedRTCSessionDescription.harden(rtcSessionDescription: rtcSessionDescription)
}
/**
* Set some more secure parameters for the session description
*/
class func harden(rtcSessionDescription: RTCSessionDescription) -> RTCSessionDescription {
var description = rtcSessionDescription.sdp
// Enforce Constant bit rate.
let cbrRegex = try! NSRegularExpression(pattern: "(a=fmtp:111 ((?!cbr=).)*)\r?\n", options: .caseInsensitive)
description = cbrRegex.stringByReplacingMatches(in: description, options: [], range: NSRange(location: 0, length: description.count), withTemplate: "$1;cbr=1\r\n")
// Strip plaintext audio-level details
// https://tools.ietf.org/html/rfc6464
let audioLevelRegex = try! NSRegularExpression(pattern: ".+urn:ietf:params:rtp-hdrext:ssrc-audio-level.*\r?\n", options: .caseInsensitive)
description = audioLevelRegex.stringByReplacingMatches(in: description, options: [], range: NSRange(location: 0, length: description.count), withTemplate: "")
return RTCSessionDescription.init(type: rtcSessionDescription.type, sdp: description)
}
}
protocol VideoCaptureSettingsDelegate: class {
var videoWidth: Int32 { get }
var videoHeight: Int32 { get }
}
class VideoCaptureController {
private let capturer: RTCCameraVideoCapturer
private weak var settingsDelegate: VideoCaptureSettingsDelegate?
private let serialQueue = DispatchQueue(label: "org.signal.videoCaptureController")
private var isUsingFrontCamera: Bool = true
public var captureSession: AVCaptureSession {
return capturer.captureSession
}
public init(capturer: RTCCameraVideoCapturer, settingsDelegate: VideoCaptureSettingsDelegate) {
self.capturer = capturer
self.settingsDelegate = settingsDelegate
}
public func startCapture() {
serialQueue.sync { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.startCaptureSync()
}
}
public func stopCapture() {
serialQueue.sync { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.capturer.stopCapture()
}
}
public func switchCamera(isUsingFrontCamera: Bool) {
serialQueue.sync { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isUsingFrontCamera = isUsingFrontCamera
strongSelf.startCaptureSync()
}
}
private func assertIsOnSerialQueue() {
if _isDebugAssertConfiguration(), #available(iOS 10.0, *) {
assertOnQueue(serialQueue)
}
}
private func startCaptureSync() {
assertIsOnSerialQueue()
let position: AVCaptureDevice.Position = isUsingFrontCamera ? .front : .back
guard let device: AVCaptureDevice = self.device(position: position) else {
owsFail("unable to find captureDevice")
return
}
guard let format: AVCaptureDevice.Format = self.format(device: device) else {
owsFail("unable to find captureDevice")
return
}
let fps = self.framesPerSecond(format: format)
capturer.startCapture(with: device, format: format, fps: fps)
}
private func device(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
let captureDevices = RTCCameraVideoCapturer.captureDevices()
guard let device = (captureDevices.first { $0.position == position }) else {
Logger.debug("unable to find desired position: \(position)")
return captureDevices.first
}
return device
}
private func format(device: AVCaptureDevice) -> AVCaptureDevice.Format? {
let formats = RTCCameraVideoCapturer.supportedFormats(for: device)
let targetWidth = settingsDelegate?.videoWidth ?? 0
let targetHeight = settingsDelegate?.videoHeight ?? 0
var selectedFormat: AVCaptureDevice.Format?
var currentDiff: Int32 = Int32.max
for format in formats {
let dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
let diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height)
if diff < currentDiff {
selectedFormat = format
currentDiff = diff
}
}
if _isDebugAssertConfiguration(), let selectedFormat = selectedFormat {
let dimension = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription)
Logger.debug("in \(#function) selected format width: \(dimension.width) height: \(dimension.height)")
}
assert(selectedFormat != nil)
return selectedFormat
}
private func framesPerSecond(format: AVCaptureDevice.Format) -> Int {
var maxFrameRate: Float64 = 0
for range in format.videoSupportedFrameRateRanges {
maxFrameRate = max(maxFrameRate, range.maxFrameRate)
}
return Int(maxFrameRate)
}
}
// MARK: Pretty Print Objc enums.
fileprivate extension RTCSignalingState {
var debugDescription: String {
switch self {
case .stable:
return "stable"
case .haveLocalOffer:
return "haveLocalOffer"
case .haveLocalPrAnswer:
return "haveLocalPrAnswer"
case .haveRemoteOffer:
return "haveRemoteOffer"
case .haveRemotePrAnswer:
return "haveRemotePrAnswer"
case .closed:
return "closed"
}
}
}
fileprivate extension RTCIceGatheringState {
var debugDescription: String {
switch self {
case .new:
return "new"
case .gathering:
return "gathering"
case .complete:
return "complete"
}
}
}
fileprivate extension RTCIceConnectionState {
var debugDescription: String {
switch self {
case .new:
return "new"
case .checking:
return "checking"
case .connected:
return "connected"
case .completed:
return "completed"
case .failed:
return "failed"
case .disconnected:
return "disconnected"
case .closed:
return "closed"
case .count:
return "count"
}
}
}