diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index efe8a7128..f839f5a68 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -149,7 +149,7 @@ typedef enum : NSUInteger { @property (nonatomic) TSThread *thread; @property (nonatomic, readonly) YapDatabaseConnection *editingDatabaseConnection; -@property (nonatomic, readonly) AudioActivity *voiceNoteAudioActivity; +@property (nonatomic, readonly) AudioActivity *recordVoiceNoteAudioActivity; @property (nonatomic, readonly) NSTimeInterval viewControllerCreatedAt; // These two properties must be updated in lockstep. @@ -286,7 +286,7 @@ typedef enum : NSUInteger { _contactShareViewHelper.delegate = self; NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ voice note", self.logTag]; - _voiceNoteAudioActivity = [[AudioActivity alloc] initWithAudioDescription:audioActivityDescription]; + _recordVoiceNoteAudioActivity = [AudioActivity recordActivityWithAudioDescription:audioActivityDescription]; } - (void)addNotificationListeners @@ -2223,12 +2223,13 @@ typedef enum : NSUInteger { // Is this player associated with this media adapter? if (self.audioAttachmentPlayer.owner == viewItem) { // Tap to pause & unpause. - [self.audioAttachmentPlayer togglePlayState]; + [self.audioAttachmentPlayer togglePlayStateWithPlaybackAudioCategory]; return; } [self.audioAttachmentPlayer stop]; self.audioAttachmentPlayer = nil; } + self.audioAttachmentPlayer = [[OWSAudioPlayer alloc] initWithMediaUrl:attachmentStream.originalMediaURL delegate:viewItem]; // Associate the player with this media adapter. @@ -3613,7 +3614,7 @@ typedef enum : NSUInteger { NSURL *fileURL = [NSURL fileURLWithPath:filepath]; // Setup audio session - BOOL configuredAudio = [OWSAudioSession.shared startRecordingAudioActivity:self.voiceNoteAudioActivity]; + BOOL configuredAudio = [OWSAudioSession.shared startAudioActivity:self.recordVoiceNoteAudioActivity]; if (!configuredAudio) { OWSFailDebug(@"Couldn't configure audio session"); [self cancelVoiceMemo]; @@ -3714,7 +3715,7 @@ typedef enum : NSUInteger { - (void)stopRecording { [self.audioRecorder stop]; - [OWSAudioSession.shared endAudioActivity:self.voiceNoteAudioActivity]; + [OWSAudioSession.shared endAudioActivity:self.recordVoiceNoteAudioActivity]; } - (void)cancelRecordingVoiceMemo diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 41f0dfb6c..cb5c9fdf6 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -668,7 +668,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele // Is this player associated with this media adapter? if audioAttachmentPlayer.owner === viewItem { // Tap to pause & unpause. - audioAttachmentPlayer.togglePlayState() + audioAttachmentPlayer.togglePlayStateWithPlaybackAudioCategory() return } audioAttachmentPlayer.stop() diff --git a/SignalMessaging/ViewControllers/MediaMessageView.swift b/SignalMessaging/ViewControllers/MediaMessageView.swift index 0c84e23c8..b5eaeced3 100644 --- a/SignalMessaging/ViewControllers/MediaMessageView.swift +++ b/SignalMessaging/ViewControllers/MediaMessageView.swift @@ -393,7 +393,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { @objc func audioPlayButtonPressed(sender: UIButton) { - audioPlayer?.togglePlayState() + audioPlayer?.togglePlayStateWithPlaybackAudioCategory() } // MARK: - OWSAudioPlayerDelegate diff --git a/SignalMessaging/attachments/OWSVideoPlayer.swift b/SignalMessaging/attachments/OWSVideoPlayer.swift index 2ba75aac6..3cbe223f6 100644 --- a/SignalMessaging/attachments/OWSVideoPlayer.swift +++ b/SignalMessaging/attachments/OWSVideoPlayer.swift @@ -22,7 +22,7 @@ public class OWSVideoPlayer: NSObject { @objc init(url: URL) { self.avPlayer = AVPlayer(url: url) - self.audioActivity = AudioActivity(audioDescription: "[OWSVideoPlayer] url:\(url)") + self.audioActivity = AudioActivity(audioDescription: "[OWSVideoPlayer] url:\(url)", options: [.playback]) super.init() @@ -42,7 +42,8 @@ public class OWSVideoPlayer: NSObject { @objc public func play() { - OWSAudioSession.shared.startPlaybackAudioActivity(self.audioActivity) + let success = OWSAudioSession.shared.startAudioActivity(self.audioActivity) + assert(success) guard let item = avPlayer.currentItem else { owsFailDebug("video player item was unexpectedly nil") diff --git a/SignalMessaging/environment/OWSAudioSession.swift b/SignalMessaging/environment/OWSAudioSession.swift index ee0d1236c..770889275 100644 --- a/SignalMessaging/environment/OWSAudioSession.swift +++ b/SignalMessaging/environment/OWSAudioSession.swift @@ -5,22 +5,61 @@ import Foundation import WebRTC +public struct AudioActivityOptions: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static let playback = AudioActivityOptions(rawValue: 1 << 0) + public static let record = AudioActivityOptions(rawValue: 1 << 1) + public static let proximitySwitchesToEarPiece = AudioActivityOptions(rawValue: 1 << 2) +} + @objc public class AudioActivity: NSObject { let audioDescription: String - override public var description: String { - return "<\(self.logTag) audioDescription: \"\(audioDescription)\">" - } + let options: AudioActivityOptions @objc public init(audioDescription: String) { self.audioDescription = audioDescription + self.options = [] + } + + public init(audioDescription: String, options: AudioActivityOptions) { + self.audioDescription = audioDescription + self.options = options } deinit { OWSAudioSession.shared.ensureAudioSessionActivationStateAfterDelay() } + + // MARK: Factory Methods + + @objc + public class func playbackActivity(audioDescription: String) -> AudioActivity { + return AudioActivity(audioDescription: audioDescription, options: .playback) + } + + @objc + public class func recordActivity(audioDescription: String) -> AudioActivity { + return AudioActivity(audioDescription: audioDescription, options: .record) + } + + @objc + public class func voiceNoteActivity(audioDescription: String) -> AudioActivity { + return AudioActivity(audioDescription: audioDescription, options: [.playback, .proximitySwitchesToEarPiece]) + } + + // MARK: + + override public var description: String { + return "<\(self.logTag) audioDescription: \"\(audioDescription)\">" + } } @objc @@ -28,79 +67,88 @@ public class OWSAudioSession: NSObject { // Force singleton access @objc public static let shared = OWSAudioSession() + private override init() {} + + public func setup() { + NotificationCenter.default.addObserver(forName: .UIDeviceProximityStateDidChange, + object: nil, + queue: nil) { [weak self] _ in + self?.ensureProximityState() + } + } + + // MARK: Dependencies + private let avAudioSession = AVAudioSession.sharedInstance() + private let device = UIDevice.current + + // MARK: + private var currentActivities: [Weak] = [] - - // Respects hardware mute switch, plays through external speaker, mixes with backround audio - // appropriate for foreground sound effects. - @objc - public func startAmbientAudioActivity(_ audioActivity: AudioActivity) { - Logger.debug("") - - objc_sync_enter(self) - defer { objc_sync_exit(self) } - - startAudioActivity(audioActivity) - guard currentActivities.count == 1 else { - // We don't want to clobber the audio capabilities configured by (e.g.) media playback or an in-progress call - Logger.info("not touching audio session since another currentActivity exists.") - return - } - - do { - try avAudioSession.setCategory(AVAudioSessionCategoryAmbient) - } catch { - owsFailDebug("failed with error: \(error)") - } - } - - // Ignores hardware mute switch, plays through external speaker - @objc - public func startPlaybackAudioActivity(_ audioActivity: AudioActivity) { - Logger.debug("") - - objc_sync_enter(self) - defer { objc_sync_exit(self) } - - startAudioActivity(audioActivity) - - do { - try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) - } catch { - owsFailDebug("failed with error: \(error)") - } + var aggregateOptions: AudioActivityOptions { + return AudioActivityOptions(self.currentActivities.compactMap { $0.value?.options }) } @objc - public func startRecordingAudioActivity(_ audioActivity: AudioActivity) -> Bool { - Logger.debug("") - - objc_sync_enter(self) - defer { objc_sync_exit(self) } - - assert(avAudioSession.recordPermission() == .granted) - - startAudioActivity(audioActivity) - - do { - try avAudioSession.setCategory(AVAudioSessionCategoryRecord) - return true - } catch { - owsFailDebug("failed with error: \(error)") - return false - } - } - - @objc - public func startAudioActivity(_ audioActivity: AudioActivity) { + public func startAudioActivity(_ audioActivity: AudioActivity) -> Bool { Logger.debug("with \(audioActivity)") objc_sync_enter(self) defer { objc_sync_exit(self) } self.currentActivities.append(Weak(value: audioActivity)) + + do { + if aggregateOptions.contains(.record) { + assert(avAudioSession.recordPermission() == .granted) + try avAudioSession.setCategory(AVAudioSessionCategoryRecord) + } else if aggregateOptions.contains(.playback) { + try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) + } else { + Logger.debug("no category option specified. Leaving category untouched.") + } + + if aggregateOptions.contains(.proximitySwitchesToEarPiece) { + self.device.isProximityMonitoringEnabled = true + self.shouldAdjustAudioForProximity = true + } else { + self.device.isProximityMonitoringEnabled = false + self.shouldAdjustAudioForProximity = false + } + ensureProximityState() + + return true + } catch { + owsFailDebug("failed with error: \(error)") + return false + } + + } + + var shouldAdjustAudioForProximity: Bool = false + func proximitySensorStateDidChange(notification: Notification) { + if shouldAdjustAudioForProximity { + ensureProximityState() + } + } + + // TODO: externally modified proximityState monitoring e.g. CallViewController + // TODO: make sure we *undo* anything as appropriate if there are concurrent audio activities + func ensureProximityState() { + if self.device.proximityState { + Logger.debug("proximityState: true") + + try! self.avAudioSession.overrideOutputAudioPort(.none) + } else { + Logger.debug("proximityState: false") + do { + try self.avAudioSession.overrideOutputAudioPort(.speaker) + } catch { + Logger.error("error: \(error)") + } + } } @objc @@ -111,6 +159,16 @@ public class OWSAudioSession: NSObject { defer { objc_sync_exit(self) } currentActivities = currentActivities.filter { return $0.value != audioActivity } + + if aggregateOptions.contains(.proximitySwitchesToEarPiece) { + self.device.isProximityMonitoringEnabled = true + self.shouldAdjustAudioForProximity = true + } else { + self.device.isProximityMonitoringEnabled = false + self.shouldAdjustAudioForProximity = false + } + ensureProximityState() + ensureAudioSessionActivationStateAfterDelay() } diff --git a/SignalMessaging/utils/OWSAudioPlayer.h b/SignalMessaging/utils/OWSAudioPlayer.h index 98a9a2f13..3fd81513f 100644 --- a/SignalMessaging/utils/OWSAudioPlayer.h +++ b/SignalMessaging/utils/OWSAudioPlayer.h @@ -43,7 +43,7 @@ typedef NS_ENUM(NSInteger, AudioPlaybackState) { - (void)pause; - (void)stop; -- (void)togglePlayState; +- (void)togglePlayStateWithPlaybackAudioCategory; @end diff --git a/SignalMessaging/utils/OWSAudioPlayer.m b/SignalMessaging/utils/OWSAudioPlayer.m index 9cc8dec90..479a21c6e 100644 --- a/SignalMessaging/utils/OWSAudioPlayer.m +++ b/SignalMessaging/utils/OWSAudioPlayer.m @@ -35,7 +35,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSURL *mediaUrl; @property (nonatomic, nullable) AVAudioPlayer *audioPlayer; @property (nonatomic, nullable) NSTimer *audioPlayerPoller; -@property (nonatomic, readonly) AudioActivity *audioActivity; +@property (nonatomic, readonly) AudioActivity *playbackAudioActivity; +@property (nonatomic, readonly) AudioActivity *currentCategoryAudioActivity; @end @@ -62,7 +63,9 @@ NS_ASSUME_NONNULL_BEGIN _mediaUrl = mediaUrl; NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ %@", self.logTag, self.mediaUrl]; - _audioActivity = [[AudioActivity alloc] initWithAudioDescription:audioActivityDescription]; + // _playbackAudioActivity = [AudioActivity playbackActivityWithAudioDescription:audioActivityDescription]; + _playbackAudioActivity = [AudioActivity voiceNoteActivityWithAudioDescription:audioActivityDescription]; + _currentCategoryAudioActivity = [[AudioActivity alloc] initWithAudioDescription:audioActivityDescription]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) @@ -91,22 +94,22 @@ NS_ASSUME_NONNULL_BEGIN - (void)playWithCurrentAudioCategory { OWSAssertIsOnMainThread(); - [OWSAudioSession.shared startAudioActivity:self.audioActivity]; - - [self play]; + [self playWithAudioActivity:self.currentCategoryAudioActivity]; } - (void)playWithPlaybackAudioCategory { OWSAssertIsOnMainThread(); - [OWSAudioSession.shared startPlaybackAudioActivity:self.audioActivity]; - - [self play]; + [self playWithAudioActivity:self.playbackAudioActivity]; } -- (void)play +- (void)playWithAudioActivity:(AudioActivity *)audioActivity { OWSAssertIsOnMainThread(); + + BOOL success = [OWSAudioSession.shared startAudioActivity:audioActivity]; + OWSAssertDebug(success); + OWSAssertDebug(self.mediaUrl); OWSAssertDebug([self.delegate audioPlaybackState] != AudioPlaybackState_Playing); @@ -157,7 +160,7 @@ NS_ASSUME_NONNULL_BEGIN [self.audioPlayerPoller invalidate]; [self.delegate setAudioProgress:(CGFloat)[self.audioPlayer currentTime] duration:(CGFloat)[self.audioPlayer duration]]; - [OWSAudioSession.shared endAudioActivity:self.audioActivity]; + [self endAudioActivities]; [DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self]; } @@ -170,18 +173,24 @@ NS_ASSUME_NONNULL_BEGIN [self.audioPlayerPoller invalidate]; [self.delegate setAudioProgress:0 duration:0]; - [OWSAudioSession.shared endAudioActivity:self.audioActivity]; + [self endAudioActivities]; [DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self]; } -- (void)togglePlayState +- (void)endAudioActivities +{ + [OWSAudioSession.shared endAudioActivity:self.playbackAudioActivity]; + [OWSAudioSession.shared endAudioActivity:self.currentCategoryAudioActivity]; +} + +- (void)togglePlayStateWithPlaybackAudioCategory { OWSAssertIsOnMainThread(); if (self.delegate.audioPlaybackState == AudioPlaybackState_Playing) { [self pause]; } else { - [self play]; + [self playWithAudioActivity:self.playbackAudioActivity]; } }