// // WebRTCClient.swift // WebRTC // // Created by Milan Bojic on 23/11/2021. // import Foundation import WebRTC import os public protocol WebRTCClientDelegate: AnyObject { func webRTCClient(_ client: WebRTCClient, didDiscoverLocalCandidate candidate: RTCIceCandidate) func webRTCClient(_ client: WebRTCClient, didChangeIceConnectionState state: RTCIceConnectionState) func webRTCClient(_ client: WebRTCClient, didChangeIceGatheringState state: RTCIceGatheringState) func webRTCClient(_ client: WebRTCClient, didChangeSignalingState state: RTCSignalingState) } open class WebRTCClient: NSObject { private var logger = os.Logger(subsystem: "BigBlueButtonMobileSDK", category: "WebRTCClient") // 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) }() public 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") } public 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 // gatherOnce will get candidates only on beginning (this is how BBB expect to have it for now, so we use this one) config.continualGatheringPolicy = .gatherOnce // 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 public func offer() async throws -> RTCSessionDescription { let constrains = RTCMediaConstraints(mandatoryConstraints: self.mediaConstrains, optionalConstraints: nil) let sdp = try await self.peerConnection.offer(for: constrains) try await self.peerConnection.setLocalDescription(sdp) return sdp } public func setRemoteSDP(remoteSDP: String) async throws { let rtcSessionDescription = RTCSessionDescription(type: RTCSdpType.answer, sdp: remoteSDP) try await self.peerConnection.setRemoteDescription(rtcSessionDescription) } 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 { public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { self.logger.info("peerConnection new signaling state: \(stateChanged.rawValue)") self.delegate?.webRTCClient(self, didChangeSignalingState: stateChanged) } public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { self.logger.info("peerConnection did add stream \(stream.streamId)") } public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { self.logger.info("peerConnection did remove stream \(stream.streamId)") } public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { self.logger.info("peerConnection should negotiate") } public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { self.logger.info("peerConnection new connection state: \(newState.rawValue)") self.delegate?.webRTCClient(self, didChangeIceConnectionState: newState) } public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) { self.logger.info("peerConnection new gathering state: \(newState.rawValue)") self.delegate?.webRTCClient(self, didChangeIceGatheringState: newState) if(newState == .complete) { self.logger.info("peerConnection new gathering state is COMPLETE") } else if(newState == .gathering) { self.logger.info("peerConnection new gathering state is GATHERING") } else if(newState == .new) { self.logger.info("peerConnection new gathering state is NEW") } } public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { self.logger.info("peerConnection discovered new candidate") self.delegate?.webRTCClient(self, didDiscoverLocalCandidate: candidate) } public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { self.logger.info("peerConnection did remove candidate(s)") } public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { self.logger.info("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 { public func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) { debugPrint("dataChannel did change state: \(dataChannel.readyState)") } public func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) { debugPrint("dataChannel did receive message with buffer: \(buffer)") } }