diff --git a/CHANGELOG.md b/CHANGELOG.md index 48eebee7ef..75c27b7aac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All user visible changes to this project will be documented in this file. This p - Upgraded [libwebrtc] to [142.0.7444.175] version. ([#256], [#254], [#248], [#260], [#264], [todo]) - Removed camera permission request in `enumerateDevices()` on Android. ([#258]) +- `AVAudioSession` is now captured only when there is at least one `PeerConnection` or local audio track, and released when there are none. ([#268]) ### Fixed @@ -51,6 +52,7 @@ All user visible changes to this project will be documented in this file. This p [#263]: https://github.com/instrumentisto/medea-flutter-webrtc/pull/263 [#264]: https://github.com/instrumentisto/medea-flutter-webrtc/pull/264 [#265]: https://github.com/instrumentisto/medea-flutter-webrtc/pull/265 +[#268]: https://github.com/instrumentisto/medea-flutter-webrtc/pull/268 [todo]: https://github.com/instrumentisto/medea-flutter-webrtc/commit/todo [142.0.7444.175]: https://github.com/instrumentisto/libwebrtc-bin/releases/tag/142.0.7444.175 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 5582e6c559..a3b4ee31a0 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -27,8 +27,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 instrumentisto-libwebrtc-bin: 06422ba9bf8b5ec14f25c1dda10109e4021decec - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - medea_flutter_webrtc: b3379af53010e6d4bd45023ba64d59864117c2fc + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + medea_flutter_webrtc: 90f34d3480bf89ebe1699c3091795b6a0ac1c2ae PODFILE CHECKSUM: 96cbd66feeeb9e49d88210ea2b661316b312dda7 diff --git a/ios/Classes/MedeaFlutterWebrtcPlugin.swift b/ios/Classes/MedeaFlutterWebrtcPlugin.swift index 1a5912a46d..41d698143d 100644 --- a/ios/Classes/MedeaFlutterWebrtcPlugin.swift +++ b/ios/Classes/MedeaFlutterWebrtcPlugin.swift @@ -20,11 +20,12 @@ public class MedeaFlutterWebrtcPlugin: NSObject, FlutterPlugin { self.state = State() self.messenger = messenger self.textures = textures + let mediaDevices = MediaDevices(state: self.state) self.peerConnectionFactory = PeerConnectionFactoryController( - messenger: self.messenger, state: self.state + messenger: self.messenger, state: self.state, mediaDevices: mediaDevices ) self.mediaDevices = MediaDevicesController( - messenger: self.messenger, mediaDevices: MediaDevices(state: self.state) + messenger: self.messenger, mediaDevices: mediaDevices ) self.videoRendererFactory = VideoRendererFactoryController( messenger: self.messenger, registry: self.textures diff --git a/ios/Classes/MediaDevices.swift b/ios/Classes/MediaDevices.swift index 681ab985d5..17f67bb04d 100644 --- a/ios/Classes/MediaDevices.swift +++ b/ios/Classes/MediaDevices.swift @@ -9,16 +9,20 @@ class MediaDevices { /// Subscribers for `onDeviceChange` callback of these `MediaDevices`. private var onDeviceChange: [() -> Void] = [] + /// Set of all existing `RTCPeerConnection`s. + private var activePeers: Set = [] + + /// Set of all existing local audio tracks. + private var activeAudioTracks: Set = [] + + /// Indicator of whether `AVAudioSession` is currently "captured". + private var isAudioSessionActive: Bool = false + /// Initializes new `MediaDevices` with the provided `State`. /// /// Subscribes on `AVAudioSession.routeChangeNotification` notifications for /// `onDeviceChange` callback firing. init(state: State) { - try? AVAudioSession.sharedInstance().setCategory( - AVAudioSession.Category.playAndRecord, - options: AVAudioSession.CategoryOptions.allowBluetooth - ) - try? AVAudioSession.sharedInstance().setActive(true) self.state = state NotificationCenter.default.addObserver( forName: AVAudioSession.routeChangeNotification, object: nil, @@ -31,6 +35,77 @@ class MediaDevices { ) } + /// Called when a new `RTCPeerConnection` is created. + /// + /// Captures the `AVAudioSession` (if its not captured already). + func peerAdded(_ id: Int) { + assert(Thread.isMainThread) + + self.activePeers.insert(id) + self.updateAudioSession() + } + + /// Called when a `RTCPeerConnection` is disposed. + /// + /// Releases the `AVAudioSession` if it's the last `RTCPeerConnection` and + /// there are no active local audio tracks. + func peerRemoved(_ id: Int) { + assert(Thread.isMainThread) + + if self.activePeers.remove(id) != nil { + self.updateAudioSession() + } + } + + /// Called when a new local audio track is created. + /// + /// Captures the `AVAudioSession` (if its not captured already). + func audioTrackAdded(_ id: String) { + assert(Thread.isMainThread) + + self.activeAudioTracks.insert(id) + self.updateAudioSession() + } + + /// Called when a local audio track is disposed. + //// + /// Releases the `AVAudioSession` if it's the last local audio track and there + /// are no `RTCPeerConnection`s. + func audioTrackRemoved(_ id: String) { + assert(Thread.isMainThread) + + if self.activeAudioTracks.remove(id) != nil { + self.updateAudioSession() + } + } + + /// Captures the `AVAudioSession` if there is at least one local audio track + /// or `RTCPeerConnection`, or releases otherwise. + /// + /// No-op if the `AVAudioSession` is in the desired state already. + private func updateAudioSession() { + assert(Thread.isMainThread) + + let shouldBeActive = !self.activePeers.isEmpty || !self.activeAudioTracks + .isEmpty + if shouldBeActive, !self.isAudioSessionActive { + try? AVAudioSession.sharedInstance().setCategory( + AVAudioSession.Category.playAndRecord, + options: AVAudioSession.CategoryOptions.allowBluetooth + ) + try? AVAudioSession.sharedInstance().setActive(true) + self.isAudioSessionActive = true + } else { + if self.isAudioSessionActive { + try? AVAudioSession.sharedInstance().setActive( + false, + options: .notifyOthersOnDeactivation + ) + self.isAudioSessionActive = false + } + } + } + /// Switches current input device to the iPhone's microphone. func setBuiltInMicAsInput() { if let routes = AVAudioSession.sharedInstance().availableInputs { @@ -159,7 +234,12 @@ class MediaDevices { withTrackId: LocalTrackIdGenerator.shared.nextId() ) let audioSource = AudioMediaTrackSourceProxy(track: track) - return audioSource.newTrack() + let trackProxy = audioSource.newTrack() + self.audioTrackAdded(trackProxy.id()) + trackProxy.onStopped(cb: { [weak self] in + self?.audioTrackRemoved(trackProxy.id()) + }) + return trackProxy } /// Creates a video `MediaStreamTrackProxy` for the provided diff --git a/ios/Classes/controller/PeerConnectionFactoryController.swift b/ios/Classes/controller/PeerConnectionFactoryController.swift index 1bd4bb20bc..6cdb3f9181 100644 --- a/ios/Classes/controller/PeerConnectionFactoryController.swift +++ b/ios/Classes/controller/PeerConnectionFactoryController.swift @@ -12,14 +12,22 @@ class PeerConnectionFactoryController { private var channel: FlutterMethodChannel /// Initializes a new `PeerConnectionFactoryController` and - /// `PeerConnectionFactoryProxy` based on the provided `State`. - init(messenger: FlutterBinaryMessenger, state: State) { + /// `PeerConnectionFactoryProxy` based on the provided `State` and + /// `MediaDevices`. + init( + messenger: FlutterBinaryMessenger, + state: State, + mediaDevices: MediaDevices + ) { let channelName = ChannelNameGenerator.name( name: "PeerConnectionFactory", id: 0 ) self.messenger = messenger - self.peerFactory = PeerConnectionFactoryProxy(state: state) + self.peerFactory = PeerConnectionFactoryProxy( + state: state, + mediaDevices: mediaDevices + ) self.channel = FlutterMethodChannel( name: channelName, binaryMessenger: messenger diff --git a/ios/Classes/proxy/PeerConnectionFactoryProxy.swift b/ios/Classes/proxy/PeerConnectionFactoryProxy.swift index 0a73a8d8ef..276ec4a491 100644 --- a/ios/Classes/proxy/PeerConnectionFactoryProxy.swift +++ b/ios/Classes/proxy/PeerConnectionFactoryProxy.swift @@ -48,10 +48,14 @@ class PeerConnectionFactoryProxy { /// Underlying native factory object of this factory. private var factory: RTCPeerConnectionFactory + /// Instance of a `MediaDevices` manager. + private var mediaDevices: MediaDevices + /// Initializes a new `PeerConnectionFactoryProxy` based on the provided - /// `State`. - init(state: State) { + /// `State` and `MediaDevices`. + init(state: State, mediaDevices: MediaDevices) { self.factory = state.getPeerFactory() + self.mediaDevices = mediaDevices } /// Returns sender capabilities of this factory. @@ -86,6 +90,13 @@ class PeerConnectionFactoryProxy { delegate: peerObserver ) let peerProxy = PeerConnectionProxy(id: id, peer: peer!) + self.mediaDevices.peerAdded(id) + peerProxy.onDispose = { [weak self] in + guard let self = self else { return } + + self.peerObservers.removeValue(forKey: id) + self.mediaDevices.peerRemoved(id) + } peerObserver.setPeer(peer: peerProxy) self.peerObservers[id] = peerObserver @@ -93,11 +104,6 @@ class PeerConnectionFactoryProxy { return peerProxy } - /// Removes the specified `PeerObserver` from the `peerObservers`. - private func remotePeerObserver(id: Int) { - self.peerObservers.removeValue(forKey: id) - } - /// Generates the next track ID. private func nextId() -> Int { self.lastPeerConnectionId += 1 diff --git a/ios/Classes/proxy/PeerConnectionProxy.swift b/ios/Classes/proxy/PeerConnectionProxy.swift index 11ed285b1a..c6039c35ad 100644 --- a/ios/Classes/proxy/PeerConnectionProxy.swift +++ b/ios/Classes/proxy/PeerConnectionProxy.swift @@ -28,6 +28,9 @@ class PeerConnectionProxy { /// Last unique ID of `RtpTransceiverProxy`s. private var lastTransceiverId: Int = 0 + /// Callback for notifying about `PeerConnectionProxy` disposal. + var onDispose: (() -> Void)? + /// Initializes a new `PeerConnectionProxy` with the provided `peer` and `id`. init(id: Int, peer: RTCPeerConnection) { self.peer = peer @@ -289,5 +292,6 @@ class PeerConnectionProxy { receiver.notifyRemoved() } self.receivers = [:] + self.onDispose?() } }