From 3d022adf4e49c20884be0fadc2023c129f6808fa Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 22 Oct 2018 17:17:05 -0600 Subject: [PATCH 1/4] WIP: audio activities --- .../ConversationViewController.m | 11 +- .../MessageDetailViewController.swift | 2 +- .../ViewControllers/MediaMessageView.swift | 2 +- .../attachments/OWSVideoPlayer.swift | 5 +- .../environment/OWSAudioSession.swift | 160 ++++++++++++------ SignalMessaging/utils/OWSAudioPlayer.h | 2 +- SignalMessaging/utils/OWSAudioPlayer.m | 35 ++-- 7 files changed, 143 insertions(+), 74 deletions(-) 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() {} - private let avAudioSession = AVAudioSession.sharedInstance() - - 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 - } + private override init() {} - do { - try avAudioSession.setCategory(AVAudioSessionCategoryAmbient) - } catch { - owsFailDebug("failed with error: \(error)") + public func setup() { + NotificationCenter.default.addObserver(forName: .UIDeviceProximityStateDidChange, + object: nil, + queue: nil) { [weak self] _ in + self?.ensureProximityState() } } - // Ignores hardware mute switch, plays through external speaker - @objc - public func startPlaybackAudioActivity(_ audioActivity: AudioActivity) { - Logger.debug("") + // MARK: Dependencies - objc_sync_enter(self) - defer { objc_sync_exit(self) } + private let avAudioSession = AVAudioSession.sharedInstance() - startAudioActivity(audioActivity) + private let device = UIDevice.current - do { - try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) - } catch { - owsFailDebug("failed with error: \(error)") - } + // MARK: + + private var currentActivities: [Weak] = [] + var aggregateOptions: AudioActivityOptions { + return AudioActivityOptions(self.currentActivities.compactMap { $0.value?.options }) } @objc - public func startRecordingAudioActivity(_ audioActivity: AudioActivity) -> Bool { - Logger.debug("") + public func startAudioActivity(_ audioActivity: AudioActivity) -> Bool { + Logger.debug("with \(audioActivity)") objc_sync_enter(self) defer { objc_sync_exit(self) } - assert(avAudioSession.recordPermission() == .granted) - - startAudioActivity(audioActivity) + self.currentActivities.append(Weak(value: audioActivity)) do { - try avAudioSession.setCategory(AVAudioSessionCategoryRecord) + 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 } - } - @objc - public func startAudioActivity(_ audioActivity: AudioActivity) { - Logger.debug("with \(audioActivity)") + } - objc_sync_enter(self) - defer { objc_sync_exit(self) } + var shouldAdjustAudioForProximity: Bool = false + func proximitySensorStateDidChange(notification: Notification) { + if shouldAdjustAudioForProximity { + ensureProximityState() + } + } - self.currentActivities.append(Weak(value: audioActivity)) + // 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]; } } From 3b4188f34b085dcffa854f48129f5df5d2a46332 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 22 Oct 2018 18:55:39 -0600 Subject: [PATCH 2/4] hoist audio session singleton to Environment --- Signal/src/AppDelegate.m | 1 + .../ConversationViewController.m | 13 ++++++++++-- Signal/src/call/CallAudioService.swift | 3 ++- Signal/src/call/NonCallKitCallUIAdaptee.swift | 15 +++++++++++--- .../Speakerbox/CallKitCallUIAdaptee.swift | 16 ++++++++++----- .../call/UserInterface/CallUIAdapter.swift | 12 +++++++++-- .../attachments/OWSVideoPlayer.swift | 14 +++++++++---- SignalMessaging/environment/AppSetup.m | 12 ++++++----- SignalMessaging/environment/Environment.h | 13 +++++++----- SignalMessaging/environment/Environment.m | 13 ++++++++---- .../environment/OWSAudioSession.swift | 20 +++++++++---------- SignalMessaging/utils/OWSAudioPlayer.m | 15 +++++++++++--- 12 files changed, 102 insertions(+), 45 deletions(-) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 35ea2be66..a8ae4382c 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -1107,6 +1107,7 @@ static NSTimeInterval launchStartedAt; // If there were any messages in our local queue which we hadn't yet processed. [SSKEnvironment.shared.messageReceiver handleAnyUnprocessedEnvelopesAsync]; [SSKEnvironment.shared.batchMessageProcessor handleAnyUnprocessedEnvelopesAsync]; + [Environment.shared.audioSession setup]; if (!Environment.shared.preferences.hasGeneratedThumbnails) { [self.primaryStorage.newDatabaseConnection diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index f839f5a68..6b05ea60b 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -289,6 +289,15 @@ typedef enum : NSUInteger { _recordVoiceNoteAudioActivity = [AudioActivity recordActivityWithAudioDescription:audioActivityDescription]; } +#pragma mark - Dependencies + +- (OWSAudioSession *)audioSession +{ + return Environment.shared.audioSession; +} + +#pragma mark + - (void)addNotificationListeners { [[NSNotificationCenter defaultCenter] addObserver:self @@ -3614,7 +3623,7 @@ typedef enum : NSUInteger { NSURL *fileURL = [NSURL fileURLWithPath:filepath]; // Setup audio session - BOOL configuredAudio = [OWSAudioSession.shared startAudioActivity:self.recordVoiceNoteAudioActivity]; + BOOL configuredAudio = [self.audioSession startAudioActivity:self.recordVoiceNoteAudioActivity]; if (!configuredAudio) { OWSFailDebug(@"Couldn't configure audio session"); [self cancelVoiceMemo]; @@ -3715,7 +3724,7 @@ typedef enum : NSUInteger { - (void)stopRecording { [self.audioRecorder stop]; - [OWSAudioSession.shared endAudioActivity:self.recordVoiceNoteAudioActivity]; + [self.audioSession endAudioActivity:self.recordVoiceNoteAudioActivity]; } - (void)cancelRecordingVoiceMemo diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 1b627e732..e99101204 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -110,8 +110,9 @@ protocol CallAudioServiceDelegate: class { private let pulseDuration = 0.2 var audioSession: OWSAudioSession { - return OWSAudioSession.shared + return Environment.shared.audioSession } + var avAudioSession: AVAudioSession { return AVAudioSession.sharedInstance() } diff --git a/Signal/src/call/NonCallKitCallUIAdaptee.swift b/Signal/src/call/NonCallKitCallUIAdaptee.swift index 930fb4f9a..7508345ab 100644 --- a/Signal/src/call/NonCallKitCallUIAdaptee.swift +++ b/Signal/src/call/NonCallKitCallUIAdaptee.swift @@ -26,13 +26,22 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { super.init() } + // MARK: Dependencies + + var audioSession: OWSAudioSession { + return Environment.shared.audioSession + } + + // MARK: + func startOutgoingCall(handle: String) -> SignalCall { AssertIsOnMainThread() let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle) // make sure we don't terminate audio session during call - OWSAudioSession.shared.startAudioActivity(call.audioActivity) + let success = self.audioSession.startAudioActivity(call.audioActivity) + assert(success) self.callService.handleOutgoingCall(call).retainUntilComplete() @@ -84,7 +93,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { return } - OWSAudioSession.shared.isRTCAudioEnabled = true + self.audioSession.isRTCAudioEnabled = true self.callService.handleAnswerCall(call) } @@ -118,7 +127,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { func recipientAcceptedCall(_ call: SignalCall) { AssertIsOnMainThread() - OWSAudioSession.shared.isRTCAudioEnabled = true + self.audioSession.isRTCAudioEnabled = true } func localHangupCall(_ call: SignalCall) { diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index 76b17e869..0fa7e0254 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -98,6 +98,12 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { self.provider.setDelegate(self, queue: nil) } + // MARK: Dependencies + + var audioSession: OWSAudioSession { + return Environment.shared.audioSession + } + // MARK: CallUIAdaptee func startOutgoingCall(handle: String) -> SignalCall { @@ -107,7 +113,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle) // make sure we don't terminate audio session during call - OWSAudioSession.shared.startAudioActivity(call.audioActivity) + self.audioSession.startAudioActivity(call.audioActivity) // Add the new outgoing call to the app's list of calls. // So we can find it in the provider delegate callbacks. @@ -379,16 +385,16 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { Logger.debug("Received") - OWSAudioSession.shared.startAudioActivity(self.audioActivity) - OWSAudioSession.shared.isRTCAudioEnabled = true + self.audioSession.startAudioActivity(self.audioActivity) + self.audioSession.isRTCAudioEnabled = true } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { AssertIsOnMainThread() Logger.debug("Received") - OWSAudioSession.shared.isRTCAudioEnabled = false - OWSAudioSession.shared.endAudioActivity(self.audioActivity) + self.audioSession.isRTCAudioEnabled = false + self.audioSession.endAudioActivity(self.audioActivity) } // MARK: - Util diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index c4d274987..a7cd703dd 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -129,11 +129,19 @@ extension CallUIAdaptee { callService.addObserverAndSyncState(observer: self) } + // MARK: Dependencies + + var audioSession: OWSAudioSession { + return Environment.shared.audioSession + } + + // MARK: + internal func reportIncomingCall(_ call: SignalCall, thread: TSContactThread) { AssertIsOnMainThread() // make sure we don't terminate audio session during call - OWSAudioSession.shared.startAudioActivity(call.audioActivity) + audioSession.startAudioActivity(call.audioActivity) let callerName = self.contactsManager.displayName(forPhoneIdentifier: call.remotePhoneNumber) adaptee.reportIncomingCall(call, callerName: callerName) @@ -181,7 +189,7 @@ extension CallUIAdaptee { AssertIsOnMainThread() if let call = call { - OWSAudioSession.shared.endAudioActivity(call.audioActivity) + self.audioSession.endAudioActivity(call.audioActivity) } } diff --git a/SignalMessaging/attachments/OWSVideoPlayer.swift b/SignalMessaging/attachments/OWSVideoPlayer.swift index 3cbe223f6..614f654b5 100644 --- a/SignalMessaging/attachments/OWSVideoPlayer.swift +++ b/SignalMessaging/attachments/OWSVideoPlayer.swift @@ -32,17 +32,23 @@ public class OWSVideoPlayer: NSObject { object: avPlayer.currentItem) } + // MARK: Dependencies + + var audioSession: OWSAudioSession { + return Environment.shared.audioSession + } + // MARK: Playback Controls @objc public func pause() { avPlayer.pause() - OWSAudioSession.shared.endAudioActivity(self.audioActivity) + audioSession.endAudioActivity(self.audioActivity) } @objc public func play() { - let success = OWSAudioSession.shared.startAudioActivity(self.audioActivity) + let success = audioSession.startAudioActivity(self.audioActivity) assert(success) guard let item = avPlayer.currentItem else { @@ -62,7 +68,7 @@ public class OWSVideoPlayer: NSObject { public func stop() { avPlayer.pause() avPlayer.seek(to: kCMTimeZero) - OWSAudioSession.shared.endAudioActivity(self.audioActivity) + audioSession.endAudioActivity(self.audioActivity) } @objc(seekToTime:) @@ -75,6 +81,6 @@ public class OWSVideoPlayer: NSObject { @objc private func playerItemDidPlayToCompletion(_ notification: Notification) { self.delegate?.videoPlayerDidPlayToCompletion(self) - OWSAudioSession.shared.endAudioActivity(self.audioActivity) + audioSession.endAudioActivity(self.audioActivity) } } diff --git a/SignalMessaging/environment/AppSetup.m b/SignalMessaging/environment/AppSetup.m index 85f476cbe..5ad9cafa4 100644 --- a/SignalMessaging/environment/AppSetup.m +++ b/SignalMessaging/environment/AppSetup.m @@ -83,14 +83,16 @@ NS_ASSUME_NONNULL_BEGIN [[OWSOutgoingReceiptManager alloc] initWithPrimaryStorage:primaryStorage]; OWSSyncManager *syncManager = [[OWSSyncManager alloc] initDefault]; + + OWSAudioSession *audioSession = [OWSAudioSession new]; OWSSounds *sounds = [[OWSSounds alloc] initWithPrimaryStorage:primaryStorage]; LockInteractionController *lockInteractionController = [[LockInteractionController alloc] initDefault]; OWSWindowManager *windowManager = [[OWSWindowManager alloc] initDefault]; - - [Environment setShared:[[Environment alloc] initWithPreferences:preferences - sounds:sounds - lockInteractionController:lockInteractionController - windowManager:windowManager]]; + [Environment setShared:[[Environment alloc] initWithAudioSession:audioSession + preferences:preferences + sounds:sounds + lockInteractionController:lockInteractionController + windowManager:windowManager]]; [SSKEnvironment setShared:[[SSKEnvironment alloc] initWithContactsManager:contactsManager messageSender:messageSender diff --git a/SignalMessaging/environment/Environment.h b/SignalMessaging/environment/Environment.h index 0a854ab0b..ee3fadd1f 100644 --- a/SignalMessaging/environment/Environment.h +++ b/SignalMessaging/environment/Environment.h @@ -5,6 +5,7 @@ #import @class LockInteractionController; +@class OWSAudioSession; @class OWSContactsManager; @class OWSPreferences; @class OWSSounds; @@ -17,16 +18,18 @@ * It also handles network configuration for testing/deployment server configurations. * **/ -// TODO: Rename to AppEnvironment? +// TODO: Rename to SMGEnvironment? @interface Environment : NSObject - (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithPreferences:(OWSPreferences *)preferences - sounds:(OWSSounds *)sounds - lockInteractionController:(LockInteractionController *)lockInteractionController - windowManager:(OWSWindowManager *)windowManager; +- (instancetype)initWithAudioSession:(OWSAudioSession *)audioSession + preferences:(OWSPreferences *)preferences + sounds:(OWSSounds *)sounds + lockInteractionController:(LockInteractionController *)lockInteractionController + windowManager:(OWSWindowManager *)windowManager; +@property (nonatomic, readonly) OWSAudioSession *audioSession; @property (nonatomic, readonly) OWSContactsManager *contactsManager; @property (nonatomic, readonly) OWSPreferences *preferences; @property (nonatomic, readonly) OWSSounds *sounds; diff --git a/SignalMessaging/environment/Environment.m b/SignalMessaging/environment/Environment.m index 544b09267..0a534c43f 100644 --- a/SignalMessaging/environment/Environment.m +++ b/SignalMessaging/environment/Environment.m @@ -11,6 +11,7 @@ static Environment *sharedEnvironment = nil; @interface Environment () +@property (nonatomic) OWSAudioSession *audioSession; @property (nonatomic) OWSContactsManager *contactsManager; @property (nonatomic) OWSPreferences *preferences; @property (nonatomic) OWSSounds *sounds; @@ -47,20 +48,24 @@ static Environment *sharedEnvironment = nil; sharedEnvironment = nil; } -- (instancetype)initWithPreferences:(OWSPreferences *)preferences - sounds:(OWSSounds *)sounds - lockInteractionController:(LockInteractionController *)lockInteractionController - windowManager:(OWSWindowManager *)windowManager { +- (instancetype)initWithAudioSession:(OWSAudioSession *)audioSession + preferences:(OWSPreferences *)preferences + sounds:(OWSSounds *)sounds + lockInteractionController:(LockInteractionController *)lockInteractionController + windowManager:(OWSWindowManager *)windowManager +{ self = [super init]; if (!self) { return self; } + OWSAssertDebug(audioSession); OWSAssertDebug(preferences); OWSAssertDebug(sounds); OWSAssertDebug(lockInteractionController); OWSAssertDebug(windowManager); + _audioSession = audioSession; _preferences = preferences; _sounds = sounds; _lockInteractionController = lockInteractionController; diff --git a/SignalMessaging/environment/OWSAudioSession.swift b/SignalMessaging/environment/OWSAudioSession.swift index 770889275..8fdde8487 100644 --- a/SignalMessaging/environment/OWSAudioSession.swift +++ b/SignalMessaging/environment/OWSAudioSession.swift @@ -35,7 +35,13 @@ public class AudioActivity: NSObject { } deinit { - OWSAudioSession.shared.ensureAudioSessionActivationStateAfterDelay() + audioSession.ensureAudioSessionActivationStateAfterDelay() + } + + // MARK: Dependencies + + var audioSession: OWSAudioSession { + return Environment.shared.audioSession } // MARK: Factory Methods @@ -65,17 +71,9 @@ public class AudioActivity: NSObject { @objc public class OWSAudioSession: NSObject { - // Force singleton access - @objc public static let shared = OWSAudioSession() - - private override init() {} - + @objc public func setup() { - NotificationCenter.default.addObserver(forName: .UIDeviceProximityStateDidChange, - object: nil, - queue: nil) { [weak self] _ in - self?.ensureProximityState() - } + NotificationCenter.default.addObserver(self, selector: #selector(proximitySensorStateDidChange(notification:)), name: .UIDeviceProximityStateDidChange, object: nil) } // MARK: Dependencies diff --git a/SignalMessaging/utils/OWSAudioPlayer.m b/SignalMessaging/utils/OWSAudioPlayer.m index 479a21c6e..d8abb7146 100644 --- a/SignalMessaging/utils/OWSAudioPlayer.m +++ b/SignalMessaging/utils/OWSAudioPlayer.m @@ -84,6 +84,15 @@ NS_ASSUME_NONNULL_BEGIN [self stop]; } +#pragma mark - Dependencies + +- (OWSAudioSession *)audioSession +{ + return Environment.shared.audioSession; +} + +#pragma mark + - (void)applicationDidEnterBackground:(NSNotification *)notification { [self stop]; @@ -107,7 +116,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssertIsOnMainThread(); - BOOL success = [OWSAudioSession.shared startAudioActivity:audioActivity]; + BOOL success = [self.audioSession startAudioActivity:audioActivity]; OWSAssertDebug(success); OWSAssertDebug(self.mediaUrl); @@ -179,8 +188,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)endAudioActivities { - [OWSAudioSession.shared endAudioActivity:self.playbackAudioActivity]; - [OWSAudioSession.shared endAudioActivity:self.currentCategoryAudioActivity]; + [self.audioSession endAudioActivity:self.playbackAudioActivity]; + [self.audioSession endAudioActivity:self.currentCategoryAudioActivity]; } - (void)togglePlayStateWithPlaybackAudioCategory From 4b4b18c625ff62e6e550cf049e12b1f2bbaed14a Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 22 Oct 2018 18:56:22 -0600 Subject: [PATCH 3/4] Proximity playsback through earpiece This is kind of unfortunate because playback doesn't allow the earpiece as an output device. --- .../environment/OWSAudioSession.swift | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/SignalMessaging/environment/OWSAudioSession.swift b/SignalMessaging/environment/OWSAudioSession.swift index 8fdde8487..4cd125388 100644 --- a/SignalMessaging/environment/OWSAudioSession.swift +++ b/SignalMessaging/environment/OWSAudioSession.swift @@ -102,6 +102,8 @@ public class OWSAudioSession: NSObject { if aggregateOptions.contains(.record) { assert(avAudioSession.recordPermission() == .granted) try avAudioSession.setCategory(AVAudioSessionCategoryRecord) + } else if aggregateOptions.contains(.proximitySwitchesToEarPiece) { + try ensureCategoryForProximityState() } else if aggregateOptions.contains(.playback) { try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) } else { @@ -110,41 +112,36 @@ public class OWSAudioSession: NSObject { 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 + @objc func proximitySensorStateDidChange(notification: Notification) { - if shouldAdjustAudioForProximity { - ensureProximityState() + do { + try ensureCategoryForProximityState() + } catch { + owsFailDebug("error in response to proximity change: \(error)") } } - // 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") + func ensureCategoryForProximityState() throws { + if aggregateOptions.contains(.proximitySwitchesToEarPiece) { + 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)") + try avAudioSession.setCategory(AVAudioSessionCategoryPlayAndRecord) + try avAudioSession.overrideOutputAudioPort(.none) + } else { + Logger.debug("proximityState: false") + try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) } } } @@ -157,16 +154,11 @@ 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 + do { + try ensureCategoryForProximityState() + } catch { + owsFailDebug("error in ensureProximityState: \(error)") } - ensureProximityState() - ensureAudioSessionActivationStateAfterDelay() } From ce9ca1bdaa29c20e815fb2c994eece3273430585 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 23 Oct 2018 08:40:09 -0600 Subject: [PATCH 4/4] audio player type --- .../OWSSoundSettingsViewController.m | 4 +- .../Cells/OWSAudioMessageView.h | 2 - .../ConversationViewController.m | 11 +-- .../MessageDetailViewController.swift | 6 +- Signal/src/call/CallAudioService.swift | 4 +- Signal/src/call/SignalCall.swift | 2 +- .../Speakerbox/CallKitCallUIAdaptee.swift | 2 +- .../ViewControllers/MediaMessageView.swift | 4 +- .../attachments/OWSVideoPlayer.swift | 2 +- .../environment/OWSAudioSession.swift | 83 +++++++------------ SignalMessaging/environment/OWSSounds.h | 4 +- SignalMessaging/environment/OWSSounds.m | 10 +-- SignalMessaging/utils/OWSAudioPlayer.h | 23 +++-- SignalMessaging/utils/OWSAudioPlayer.m | 35 ++++---- 14 files changed, 81 insertions(+), 111 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.m index ed1addd87..c218cd34d 100644 --- a/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.m @@ -112,10 +112,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)soundWasSelected:(OWSSound)sound { [self.audioPlayer stop]; - self.audioPlayer = [OWSSounds audioPlayerForSound:sound]; + self.audioPlayer = [OWSSounds audioPlayerForSound:sound audioBehavior:OWSAudioBehavior_Playback]; // Suppress looping in this view. self.audioPlayer.isLooping = NO; - [self.audioPlayer playWithPlaybackAudioCategory]; + [self.audioPlayer play]; if (self.currentSound == sound) { return; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.h index 1d0c98e6e..f7be8be2e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.h @@ -2,8 +2,6 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -#import "OWSAudioPlayer.h" - NS_ASSUME_NONNULL_BEGIN @class ConversationStyle; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 6b05ea60b..15d8d3cc9 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 *recordVoiceNoteAudioActivity; +@property (nonatomic, readonly) OWSAudioActivity *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]; - _recordVoiceNoteAudioActivity = [AudioActivity recordActivityWithAudioDescription:audioActivityDescription]; + _recordVoiceNoteAudioActivity = [[OWSAudioActivity alloc] initWithAudioDescription:audioActivityDescription behavior:OWSAudioBehavior_PlayAndRecord]; } #pragma mark - Dependencies @@ -2232,7 +2232,7 @@ typedef enum : NSUInteger { // Is this player associated with this media adapter? if (self.audioAttachmentPlayer.owner == viewItem) { // Tap to pause & unpause. - [self.audioAttachmentPlayer togglePlayStateWithPlaybackAudioCategory]; + [self.audioAttachmentPlayer togglePlayState]; return; } [self.audioAttachmentPlayer stop]; @@ -2240,10 +2240,11 @@ typedef enum : NSUInteger { } self.audioAttachmentPlayer = - [[OWSAudioPlayer alloc] initWithMediaUrl:attachmentStream.originalMediaURL delegate:viewItem]; + [[OWSAudioPlayer alloc] initWithMediaUrl:attachmentStream.originalMediaURL audioBehavior:OWSAudioBehavior_AudioMessagePlayback delegate:viewItem]; + // Associate the player with this media adapter. self.audioAttachmentPlayer.owner = viewItem; - [self.audioAttachmentPlayer playWithPlaybackAudioCategory]; + [self.audioAttachmentPlayer play]; } - (void)didTapTruncatedTextMessage:(id)conversationItem diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index cb5c9fdf6..b08d9802d 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -668,19 +668,19 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele // Is this player associated with this media adapter? if audioAttachmentPlayer.owner === viewItem { // Tap to pause & unpause. - audioAttachmentPlayer.togglePlayStateWithPlaybackAudioCategory() + audioAttachmentPlayer.togglePlayState() return } audioAttachmentPlayer.stop() self.audioAttachmentPlayer = nil } - let audioAttachmentPlayer = OWSAudioPlayer(mediaUrl: mediaURL, delegate: viewItem) + let audioAttachmentPlayer = OWSAudioPlayer(mediaUrl: mediaURL, audioBehavior: .audioMessagePlayback, delegate: viewItem) self.audioAttachmentPlayer = audioAttachmentPlayer // Associate the player with this media adapter. audioAttachmentPlayer.owner = viewItem - audioAttachmentPlayer.playWithPlaybackAudioCategory() + audioAttachmentPlayer.play() } func didTapTruncatedTextMessage(_ conversationItem: ConversationViewItem) { diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index e99101204..8ce926ce7 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -397,7 +397,7 @@ protocol CallAudioServiceDelegate: class { } private func play(sound: OWSSound) { - guard let newPlayer = OWSSounds.audioPlayer(for: sound) else { + guard let newPlayer = OWSSounds.audioPlayer(for: sound, audioBehavior: .call) else { owsFailDebug("unable to build player for sound: \(OWSSounds.displayName(for: sound))") return } @@ -407,7 +407,7 @@ protocol CallAudioServiceDelegate: class { // we're playing the same sound, since the player is memoized on the sound instance, we'd otherwise // stop the sound we just started. self.currentPlayer?.stop() - newPlayer.playWithCurrentAudioCategory() + newPlayer.play() self.currentPlayer = newPlayer } diff --git a/Signal/src/call/SignalCall.swift b/Signal/src/call/SignalCall.swift index 51a018803..370960238 100644 --- a/Signal/src/call/SignalCall.swift +++ b/Signal/src/call/SignalCall.swift @@ -161,7 +161,7 @@ protocol CallObserver: class { self.state = state self.remotePhoneNumber = remotePhoneNumber self.thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber) - self.audioActivity = AudioActivity(audioDescription: "[SignalCall] with \(remotePhoneNumber)") + self.audioActivity = AudioActivity(audioDescription: "[SignalCall] with \(remotePhoneNumber)", behavior: .call) } // A string containing the three identifiers for this call. diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index 0fa7e0254..6e0970623 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -88,7 +88,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { self.provider = type(of: self).sharedProvider(useSystemCallLog: useSystemCallLog) - self.audioActivity = AudioActivity(audioDescription: "[CallKitCallUIAdaptee]") + self.audioActivity = AudioActivity(audioDescription: "[CallKitCallUIAdaptee]", behavior: .call) self.showNamesOnCallScreen = showNamesOnCallScreen super.init() diff --git a/SignalMessaging/ViewControllers/MediaMessageView.swift b/SignalMessaging/ViewControllers/MediaMessageView.swift index b5eaeced3..02e7b1a51 100644 --- a/SignalMessaging/ViewControllers/MediaMessageView.swift +++ b/SignalMessaging/ViewControllers/MediaMessageView.swift @@ -131,7 +131,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return } - audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, delegate: self) + audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) var subviews = [UIView]() @@ -393,7 +393,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { @objc func audioPlayButtonPressed(sender: UIButton) { - audioPlayer?.togglePlayStateWithPlaybackAudioCategory() + audioPlayer?.togglePlayState() } // MARK: - OWSAudioPlayerDelegate diff --git a/SignalMessaging/attachments/OWSVideoPlayer.swift b/SignalMessaging/attachments/OWSVideoPlayer.swift index 614f654b5..c32e899fd 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)", options: [.playback]) + self.audioActivity = AudioActivity(audioDescription: "[OWSVideoPlayer] url:\(url)", behavior: .playback) super.init() diff --git a/SignalMessaging/environment/OWSAudioSession.swift b/SignalMessaging/environment/OWSAudioSession.swift index 4cd125388..ef4868558 100644 --- a/SignalMessaging/environment/OWSAudioSession.swift +++ b/SignalMessaging/environment/OWSAudioSession.swift @@ -5,33 +5,16 @@ 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 +@objc(OWSAudioActivity) public class AudioActivity: NSObject { let audioDescription: String - let options: AudioActivityOptions + let behavior: OWSAudioBehavior @objc - public init(audioDescription: String) { + public init(audioDescription: String, behavior: OWSAudioBehavior) { self.audioDescription = audioDescription - self.options = [] - } - - public init(audioDescription: String, options: AudioActivityOptions) { - self.audioDescription = audioDescription - self.options = options + self.behavior = behavior } deinit { @@ -44,23 +27,6 @@ public class AudioActivity: NSObject { return Environment.shared.audioSession } - // 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 { @@ -85,8 +51,8 @@ public class OWSAudioSession: NSObject { // MARK: private var currentActivities: [Weak] = [] - var aggregateOptions: AudioActivityOptions { - return AudioActivityOptions(self.currentActivities.compactMap { $0.value?.options }) + var aggregateBehaviors: Set { + return Set(self.currentActivities.compactMap { $0.value?.behavior }) } @objc @@ -99,21 +65,28 @@ public class OWSAudioSession: NSObject { self.currentActivities.append(Weak(value: audioActivity)) do { - if aggregateOptions.contains(.record) { - assert(avAudioSession.recordPermission() == .granted) - try avAudioSession.setCategory(AVAudioSessionCategoryRecord) - } else if aggregateOptions.contains(.proximitySwitchesToEarPiece) { - try ensureCategoryForProximityState() - } 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 + if aggregateBehaviors.contains(.call) { + // Do nothing while on a call. + // WebRTC/CallAudioService manages call audio + // Eventually it would be nice to consolidate more of the audio + // session handling. } else { - self.device.isProximityMonitoringEnabled = false + if aggregateBehaviors.contains(.playAndRecord) { + assert(avAudioSession.recordPermission() == .granted) + try avAudioSession.setCategory(AVAudioSessionCategoryRecord) + } else if aggregateBehaviors.contains(.audioMessagePlayback) { + try ensureCategoryForProximityState() + } else if aggregateBehaviors.contains(.playback) { + try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) + } else { + owsFailDebug("no category option specified. Leaving category untouched.") + } + + if aggregateBehaviors.contains(.audioMessagePlayback) { + self.device.isProximityMonitoringEnabled = true + } else { + self.device.isProximityMonitoringEnabled = false + } } return true @@ -133,7 +106,7 @@ public class OWSAudioSession: NSObject { } func ensureCategoryForProximityState() throws { - if aggregateOptions.contains(.proximitySwitchesToEarPiece) { + if aggregateBehaviors.contains(.audioMessagePlayback) { if self.device.proximityState { Logger.debug("proximityState: true") diff --git a/SignalMessaging/environment/OWSSounds.h b/SignalMessaging/environment/OWSSounds.h index 4cee5e581..f7ff36072 100644 --- a/SignalMessaging/environment/OWSSounds.h +++ b/SignalMessaging/environment/OWSSounds.h @@ -2,6 +2,7 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // +#import "OWSAudioPlayer.h" #import NS_ASSUME_NONNULL_BEGIN @@ -68,7 +69,8 @@ typedef NS_ENUM(NSUInteger, OWSSound) { #pragma mark - AudioPlayer -+ (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound; ++ (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound + audioBehavior:(OWSAudioBehavior)audioBehavior; @end diff --git a/SignalMessaging/environment/OWSSounds.m b/SignalMessaging/environment/OWSSounds.m index 3ef0bf1da..506b7bd8e 100644 --- a/SignalMessaging/environment/OWSSounds.m +++ b/SignalMessaging/environment/OWSSounds.m @@ -375,17 +375,13 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob } + (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound + audioBehavior:(OWSAudioBehavior)audioBehavior; { - return [self audioPlayerForSound:sound quiet:NO]; -} - -+ (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound quiet:(BOOL)quiet -{ - NSURL *_Nullable soundURL = [OWSSounds soundURLForSound:sound quiet:(BOOL)quiet]; + NSURL *_Nullable soundURL = [OWSSounds soundURLForSound:sound quiet:NO]; if (!soundURL) { return nil; } - OWSAudioPlayer *player = [[OWSAudioPlayer alloc] initWithMediaUrl:soundURL]; + OWSAudioPlayer *player = [[OWSAudioPlayer alloc] initWithMediaUrl:soundURL audioBehavior:audioBehavior]; if ([self shouldAudioPlayerLoopForSound:sound]) { player.isLooping = YES; } diff --git a/SignalMessaging/utils/OWSAudioPlayer.h b/SignalMessaging/utils/OWSAudioPlayer.h index 3fd81513f..93353c63c 100644 --- a/SignalMessaging/utils/OWSAudioPlayer.h +++ b/SignalMessaging/utils/OWSAudioPlayer.h @@ -21,6 +21,14 @@ typedef NS_ENUM(NSInteger, AudioPlaybackState) { #pragma mark - +typedef NS_ENUM(NSUInteger, OWSAudioBehavior) { + OWSAudioBehavior_Unknown, + OWSAudioBehavior_Playback, + OWSAudioBehavior_AudioMessagePlayback, + OWSAudioBehavior_PlayAndRecord, + OWSAudioBehavior_Call, +}; + @interface OWSAudioPlayer : NSObject @property (nonatomic, readonly, weak) id delegate; @@ -31,19 +39,16 @@ typedef NS_ENUM(NSInteger, AudioPlaybackState) { @property (nonatomic) BOOL isLooping; -- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl; - -- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl delegate:(id)delegate; - -// respects silent switch -- (void)playWithCurrentAudioCategory; +- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl audioBehavior:(OWSAudioBehavior)audioBehavior; -// will ensure sound is audible, even if silent switch is enabled -- (void)playWithPlaybackAudioCategory; +- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl + audioBehavior:(OWSAudioBehavior)audioBehavior + delegate:(id)delegate; +- (void)play; - (void)pause; - (void)stop; -- (void)togglePlayStateWithPlaybackAudioCategory; +- (void)togglePlayState; @end diff --git a/SignalMessaging/utils/OWSAudioPlayer.m b/SignalMessaging/utils/OWSAudioPlayer.m index d8abb7146..e44963ef4 100644 --- a/SignalMessaging/utils/OWSAudioPlayer.m +++ b/SignalMessaging/utils/OWSAudioPlayer.m @@ -35,8 +35,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSURL *mediaUrl; @property (nonatomic, nullable) AVAudioPlayer *audioPlayer; @property (nonatomic, nullable) NSTimer *audioPlayerPoller; -@property (nonatomic, readonly) AudioActivity *playbackAudioActivity; -@property (nonatomic, readonly) AudioActivity *currentCategoryAudioActivity; +@property (nonatomic, readonly) OWSAudioActivity *audioActivity; @end @@ -45,11 +44,14 @@ NS_ASSUME_NONNULL_BEGIN @implementation OWSAudioPlayer - (instancetype)initWithMediaUrl:(NSURL *)mediaUrl + audioBehavior:(OWSAudioBehavior)audioBehavior { - return [self initWithMediaUrl:mediaUrl delegate:[OWSAudioPlayerDelegateStub new]]; + return [self initWithMediaUrl:mediaUrl audioBehavior:audioBehavior delegate:[OWSAudioPlayerDelegateStub new]]; } -- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl delegate:(id)delegate +- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl + audioBehavior:(OWSAudioBehavior)audioBehavior + delegate:(id)delegate { self = [super init]; if (!self) { @@ -59,13 +61,11 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(mediaUrl); OWSAssertDebug(delegate); - _delegate = delegate; _mediaUrl = mediaUrl; + _delegate = delegate; NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ %@", self.logTag, self.mediaUrl]; - // _playbackAudioActivity = [AudioActivity playbackActivityWithAudioDescription:audioActivityDescription]; - _playbackAudioActivity = [AudioActivity voiceNoteActivityWithAudioDescription:audioActivityDescription]; - _currentCategoryAudioActivity = [[AudioActivity alloc] initWithAudioDescription:audioActivityDescription]; + _audioActivity = [[OWSAudioActivity alloc] initWithAudioDescription:audioActivityDescription behavior:audioBehavior]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) @@ -100,19 +100,15 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Methods -- (void)playWithCurrentAudioCategory +- (void)play { - OWSAssertIsOnMainThread(); - [self playWithAudioActivity:self.currentCategoryAudioActivity]; -} -- (void)playWithPlaybackAudioCategory -{ + // get current audio activity OWSAssertIsOnMainThread(); - [self playWithAudioActivity:self.playbackAudioActivity]; + [self playWithAudioActivity:self.audioActivity]; } -- (void)playWithAudioActivity:(AudioActivity *)audioActivity +- (void)playWithAudioActivity:(OWSAudioActivity *)audioActivity { OWSAssertIsOnMainThread(); @@ -188,18 +184,17 @@ NS_ASSUME_NONNULL_BEGIN - (void)endAudioActivities { - [self.audioSession endAudioActivity:self.playbackAudioActivity]; - [self.audioSession endAudioActivity:self.currentCategoryAudioActivity]; + [self.audioSession endAudioActivity:self.audioActivity]; } -- (void)togglePlayStateWithPlaybackAudioCategory +- (void)togglePlayState { OWSAssertIsOnMainThread(); if (self.delegate.audioPlaybackState == AudioPlaybackState_Playing) { [self pause]; } else { - [self playWithAudioActivity:self.playbackAudioActivity]; + [self playWithAudioActivity:self.audioActivity]; } }