From c11e72d3cd80bec3fbbcbb314c55c580d46c8039 Mon Sep 17 00:00:00 2001 From: Milan Bojic Date: Mon, 21 Mar 2022 09:31:08 +0100 Subject: [PATCH] Add WebRTCClient.swift to common/ios/Classes folder --- common/ios/Classes/WebRTCClient.swift | 264 ++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 common/ios/Classes/WebRTCClient.swift diff --git a/common/ios/Classes/WebRTCClient.swift b/common/ios/Classes/WebRTCClient.swift new file mode 100644 index 0000000..ab3e3ce --- /dev/null +++ b/common/ios/Classes/WebRTCClient.swift @@ -0,0 +1,264 @@ +// +// WebRTCClient.swift +// WebRTC +// +// Created by Milan Bojic on 23/11/2021. +// + +import Foundation +import WebRTC + +protocol WebRTCClientDelegate: AnyObject { + func webRTCClient(_ client: WebRTCClient, didDiscoverLocalCandidate candidate: RTCIceCandidate) + func webRTCClient(_ client: WebRTCClient, didChangeConnectionState state: RTCIceConnectionState) +} + +final class WebRTCClient: NSObject { + + // The `RTCPeerConnectionFactory` is in charge of creating new RTCPeerConnection instances. + // A new RTCPeerConnection should be created every new call, but the factory is shared. + private static let factory: RTCPeerConnectionFactory = { + RTCInitializeSSL() + let videoEncoderFactory = RTCDefaultVideoEncoderFactory() + let videoDecoderFactory = RTCDefaultVideoDecoderFactory() + videoEncoderFactory.preferredCodec = RTCVideoCodecInfo(name: kRTCVideoCodecVp8Name) + return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory) + }() + + weak var delegate: WebRTCClientDelegate? + private let peerConnection: RTCPeerConnection + private let rtcAudioSession = RTCAudioSession.sharedInstance() + private let audioQueue = DispatchQueue(label: "audio") + private let mediaConstrains = [kRTCMediaConstraintsOfferToReceiveAudio: kRTCMediaConstraintsValueTrue, + kRTCMediaConstraintsOfferToReceiveVideo: kRTCMediaConstraintsValueTrue] + private var videoSource: RTCVideoSource? + private var videoCapturer: RTCVideoCapturer? + private var localVideoTrack: RTCVideoTrack? + + @available(*, unavailable) + override init() { + fatalError("WebRTCClient:init is unavailable") + } + + required init(iceServers: [String]) { + 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 + + // Define media constraints. DtlsSrtpKeyAgreement is required to be true to be able to connect with web browsers. + let constraints = RTCMediaConstraints(mandatoryConstraints: nil, + optionalConstraints: ["DtlsSrtpKeyAgreement":kRTCMediaConstraintsValueTrue]) + + guard let peerConnection = WebRTCClient.factory.peerConnection(with: config, constraints: constraints, delegate: nil) else { + fatalError("Could not create new RTCPeerConnection") + } + + self.peerConnection = peerConnection + + super.init() + createMediaSenders() + configureAudioSession() + self.peerConnection.delegate = self + } + + // MARK: Signaling + + 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) + }) + } + } + + func set(remoteSdp: RTCSessionDescription, completion: @escaping (Error?) -> ()) { + self.peerConnection.setRemoteDescription(remoteSdp, completionHandler: completion) + } + + func set(remoteCandidate: RTCIceCandidate, completion: @escaping (Error?) -> ()) { + self.peerConnection.add(remoteCandidate, completionHandler: completion) + } + + // MARK: Media + + func push(videoFrame: RTCVideoFrame) { + guard videoCapturer != nil, videoSource != nil else { return } + videoSource!.capturer(videoCapturer!, didCapture: videoFrame) + print("RTCVideoFrame pushed to server.") + } + + private func configureAudioSession() { + self.rtcAudioSession.lockForConfiguration() + do { + try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue) + try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue) + } catch let error { + debugPrint("Error changing AVAudioSession category: \(error)") + } + self.rtcAudioSession.unlockForConfiguration() + } + + private func createMediaSenders() { + let streamId = "stream" + + // Audio + let audioTrack = self.createAudioTrack() + self.peerConnection.add(audioTrack, streamIds: [streamId]) + + // Video + let videoTrack = self.createVideoTrack() + self.localVideoTrack = videoTrack + self.peerConnection.add(videoTrack, streamIds: [streamId]) + } + + private func createAudioTrack() -> RTCAudioTrack { + let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) + let audioSource = WebRTCClient.factory.audioSource(with: audioConstrains) + let audioTrack = WebRTCClient.factory.audioTrack(with: audioSource, trackId: "audio0") + return audioTrack + } + + private func createVideoTrack() -> RTCVideoTrack { + videoSource = WebRTCClient.factory.videoSource(forScreenCast: true) + videoCapturer = RTCVideoCapturer(delegate: videoSource!) + videoSource!.adaptOutputFormat(toWidth: 600, height: 800, fps: 15) + let videoTrack = WebRTCClient.factory.videoTrack(with: videoSource!, trackId: "video0") + videoTrack.isEnabled = true + return videoTrack + } +} + +// MARK: RTCPeerConnectionDelegate Methods + +extension WebRTCClient: RTCPeerConnectionDelegate { + + func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { + debugPrint("peerConnection new signaling state: \(stateChanged)") + } + + func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { + debugPrint("peerConnection did add stream \(stream)") + } + + func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { + debugPrint("peerConnection did remove stream \(stream)") + } + + func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { + debugPrint("peerConnection should negotiate") + } + + func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { + debugPrint("peerConnection new connection state: \(newState)") + self.delegate?.webRTCClient(self, didChangeConnectionState: newState) + } + + func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) { + debugPrint("peerConnection new gathering state: \(newState)") + } + + func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { + debugPrint("peerConnection discovered new candidate") + self.delegate?.webRTCClient(self, didDiscoverLocalCandidate: candidate) + } + + func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { + debugPrint("peerConnection did remove candidate(s)") + } + + func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { + debugPrint("peerConnection did open data channel") + } +} + +extension WebRTCClient { + private func setTrackEnabled(_ type: T.Type, isEnabled: Bool) { + peerConnection.transceivers + .compactMap { return $0.sender.track as? T } + .forEach { $0.isEnabled = isEnabled } + } +} + +// MARK: - Video control + +extension WebRTCClient { + func hideVideo() { + self.setVideoEnabled(false) + } + func showVideo() { + self.setVideoEnabled(true) + } + private func setVideoEnabled(_ isEnabled: Bool) { + setTrackEnabled(RTCVideoTrack.self, isEnabled: isEnabled) + } +} + +// MARK:- Audio control + +extension WebRTCClient { + func muteAudio() { + self.setAudioEnabled(false) + } + + func unmuteAudio() { + self.setAudioEnabled(true) + } + + // Fallback to the default playing device: headphones/bluetooth/ear speaker + func speakerOff() { + self.audioQueue.async { [weak self] in + guard let self = self else { + return + } + + self.rtcAudioSession.lockForConfiguration() + do { + try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue) + try self.rtcAudioSession.overrideOutputAudioPort(.none) + } catch let error { + debugPrint("Error setting AVAudioSession category: \(error)") + } + self.rtcAudioSession.unlockForConfiguration() + } + } + + // Force speaker + func speakerOn() { + self.audioQueue.async { [weak self] in + guard let self = self else { + return + } + + self.rtcAudioSession.lockForConfiguration() + do { + try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue) + try self.rtcAudioSession.overrideOutputAudioPort(.speaker) + try self.rtcAudioSession.setActive(true) + } catch let error { + debugPrint("Couldn't force audio to speaker: \(error)") + } + self.rtcAudioSession.unlockForConfiguration() + } + } + + private func setAudioEnabled(_ isEnabled: Bool) { + setTrackEnabled(RTCAudioTrack.self, isEnabled: isEnabled) + } +} + +extension WebRTCClient: RTCDataChannelDelegate { + func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) { + debugPrint("dataChannel did change state: \(dataChannel.readyState)") + } + + func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) { + debugPrint("dataChannel did receive message with buffer: \(buffer)") + } +}