WIP: audio activities

This commit is contained in:
Michael Kirk 2018-10-22 17:17:05 -06:00
parent 14f2b89367
commit 3d022adf4e
7 changed files with 155 additions and 86 deletions

View file

@ -149,7 +149,7 @@ typedef enum : NSUInteger {
@property (nonatomic) TSThread *thread; @property (nonatomic) TSThread *thread;
@property (nonatomic, readonly) YapDatabaseConnection *editingDatabaseConnection; @property (nonatomic, readonly) YapDatabaseConnection *editingDatabaseConnection;
@property (nonatomic, readonly) AudioActivity *voiceNoteAudioActivity; @property (nonatomic, readonly) AudioActivity *recordVoiceNoteAudioActivity;
@property (nonatomic, readonly) NSTimeInterval viewControllerCreatedAt; @property (nonatomic, readonly) NSTimeInterval viewControllerCreatedAt;
// These two properties must be updated in lockstep. // These two properties must be updated in lockstep.
@ -286,7 +286,7 @@ typedef enum : NSUInteger {
_contactShareViewHelper.delegate = self; _contactShareViewHelper.delegate = self;
NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ voice note", self.logTag]; NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ voice note", self.logTag];
_voiceNoteAudioActivity = [[AudioActivity alloc] initWithAudioDescription:audioActivityDescription]; _recordVoiceNoteAudioActivity = [AudioActivity recordActivityWithAudioDescription:audioActivityDescription];
} }
- (void)addNotificationListeners - (void)addNotificationListeners
@ -2223,12 +2223,13 @@ typedef enum : NSUInteger {
// Is this player associated with this media adapter? // Is this player associated with this media adapter?
if (self.audioAttachmentPlayer.owner == viewItem) { if (self.audioAttachmentPlayer.owner == viewItem) {
// Tap to pause & unpause. // Tap to pause & unpause.
[self.audioAttachmentPlayer togglePlayState]; [self.audioAttachmentPlayer togglePlayStateWithPlaybackAudioCategory];
return; return;
} }
[self.audioAttachmentPlayer stop]; [self.audioAttachmentPlayer stop];
self.audioAttachmentPlayer = nil; self.audioAttachmentPlayer = nil;
} }
self.audioAttachmentPlayer = self.audioAttachmentPlayer =
[[OWSAudioPlayer alloc] initWithMediaUrl:attachmentStream.originalMediaURL delegate:viewItem]; [[OWSAudioPlayer alloc] initWithMediaUrl:attachmentStream.originalMediaURL delegate:viewItem];
// Associate the player with this media adapter. // Associate the player with this media adapter.
@ -3613,7 +3614,7 @@ typedef enum : NSUInteger {
NSURL *fileURL = [NSURL fileURLWithPath:filepath]; NSURL *fileURL = [NSURL fileURLWithPath:filepath];
// Setup audio session // Setup audio session
BOOL configuredAudio = [OWSAudioSession.shared startRecordingAudioActivity:self.voiceNoteAudioActivity]; BOOL configuredAudio = [OWSAudioSession.shared startAudioActivity:self.recordVoiceNoteAudioActivity];
if (!configuredAudio) { if (!configuredAudio) {
OWSFailDebug(@"Couldn't configure audio session"); OWSFailDebug(@"Couldn't configure audio session");
[self cancelVoiceMemo]; [self cancelVoiceMemo];
@ -3714,7 +3715,7 @@ typedef enum : NSUInteger {
- (void)stopRecording - (void)stopRecording
{ {
[self.audioRecorder stop]; [self.audioRecorder stop];
[OWSAudioSession.shared endAudioActivity:self.voiceNoteAudioActivity]; [OWSAudioSession.shared endAudioActivity:self.recordVoiceNoteAudioActivity];
} }
- (void)cancelRecordingVoiceMemo - (void)cancelRecordingVoiceMemo

View file

@ -668,7 +668,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
// Is this player associated with this media adapter? // Is this player associated with this media adapter?
if audioAttachmentPlayer.owner === viewItem { if audioAttachmentPlayer.owner === viewItem {
// Tap to pause & unpause. // Tap to pause & unpause.
audioAttachmentPlayer.togglePlayState() audioAttachmentPlayer.togglePlayStateWithPlaybackAudioCategory()
return return
} }
audioAttachmentPlayer.stop() audioAttachmentPlayer.stop()

View file

@ -393,7 +393,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
@objc @objc
func audioPlayButtonPressed(sender: UIButton) { func audioPlayButtonPressed(sender: UIButton) {
audioPlayer?.togglePlayState() audioPlayer?.togglePlayStateWithPlaybackAudioCategory()
} }
// MARK: - OWSAudioPlayerDelegate // MARK: - OWSAudioPlayerDelegate

View file

@ -22,7 +22,7 @@ public class OWSVideoPlayer: NSObject {
@objc init(url: URL) { @objc init(url: URL) {
self.avPlayer = AVPlayer(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() super.init()
@ -42,7 +42,8 @@ public class OWSVideoPlayer: NSObject {
@objc @objc
public func play() { public func play() {
OWSAudioSession.shared.startPlaybackAudioActivity(self.audioActivity) let success = OWSAudioSession.shared.startAudioActivity(self.audioActivity)
assert(success)
guard let item = avPlayer.currentItem else { guard let item = avPlayer.currentItem else {
owsFailDebug("video player item was unexpectedly nil") owsFailDebug("video player item was unexpectedly nil")

View file

@ -5,22 +5,61 @@
import Foundation import Foundation
import WebRTC 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
public class AudioActivity: NSObject { public class AudioActivity: NSObject {
let audioDescription: String let audioDescription: String
override public var description: String { let options: AudioActivityOptions
return "<\(self.logTag) audioDescription: \"\(audioDescription)\">"
}
@objc @objc
public init(audioDescription: String) { public init(audioDescription: String) {
self.audioDescription = audioDescription self.audioDescription = audioDescription
self.options = []
}
public init(audioDescription: String, options: AudioActivityOptions) {
self.audioDescription = audioDescription
self.options = options
} }
deinit { deinit {
OWSAudioSession.shared.ensureAudioSessionActivationStateAfterDelay() 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 @objc
@ -28,79 +67,88 @@ public class OWSAudioSession: NSObject {
// Force singleton access // Force singleton access
@objc public static let shared = OWSAudioSession() @objc public static let shared = OWSAudioSession()
private override init() {} 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 avAudioSession = AVAudioSession.sharedInstance()
private let device = UIDevice.current
// MARK:
private var currentActivities: [Weak<AudioActivity>] = [] private var currentActivities: [Weak<AudioActivity>] = []
var aggregateOptions: AudioActivityOptions {
// Respects hardware mute switch, plays through external speaker, mixes with backround audio return AudioActivityOptions(self.currentActivities.compactMap { $0.value?.options })
// 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)")
}
} }
@objc @objc
public func startRecordingAudioActivity(_ audioActivity: AudioActivity) -> Bool { public func startAudioActivity(_ 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) {
Logger.debug("with \(audioActivity)") Logger.debug("with \(audioActivity)")
objc_sync_enter(self) objc_sync_enter(self)
defer { objc_sync_exit(self) } defer { objc_sync_exit(self) }
self.currentActivities.append(Weak(value: audioActivity)) 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 @objc
@ -111,6 +159,16 @@ public class OWSAudioSession: NSObject {
defer { objc_sync_exit(self) } defer { objc_sync_exit(self) }
currentActivities = currentActivities.filter { return $0.value != audioActivity } 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() ensureAudioSessionActivationStateAfterDelay()
} }

View file

@ -43,7 +43,7 @@ typedef NS_ENUM(NSInteger, AudioPlaybackState) {
- (void)pause; - (void)pause;
- (void)stop; - (void)stop;
- (void)togglePlayState; - (void)togglePlayStateWithPlaybackAudioCategory;
@end @end

View file

@ -35,7 +35,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) NSURL *mediaUrl; @property (nonatomic, readonly) NSURL *mediaUrl;
@property (nonatomic, nullable) AVAudioPlayer *audioPlayer; @property (nonatomic, nullable) AVAudioPlayer *audioPlayer;
@property (nonatomic, nullable) NSTimer *audioPlayerPoller; @property (nonatomic, nullable) NSTimer *audioPlayerPoller;
@property (nonatomic, readonly) AudioActivity *audioActivity; @property (nonatomic, readonly) AudioActivity *playbackAudioActivity;
@property (nonatomic, readonly) AudioActivity *currentCategoryAudioActivity;
@end @end
@ -62,7 +63,9 @@ NS_ASSUME_NONNULL_BEGIN
_mediaUrl = mediaUrl; _mediaUrl = mediaUrl;
NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ %@", self.logTag, self.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 [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:) selector:@selector(applicationDidEnterBackground:)
@ -91,22 +94,22 @@ NS_ASSUME_NONNULL_BEGIN
- (void)playWithCurrentAudioCategory - (void)playWithCurrentAudioCategory
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
[OWSAudioSession.shared startAudioActivity:self.audioActivity]; [self playWithAudioActivity:self.currentCategoryAudioActivity];
[self play];
} }
- (void)playWithPlaybackAudioCategory - (void)playWithPlaybackAudioCategory
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
[OWSAudioSession.shared startPlaybackAudioActivity:self.audioActivity]; [self playWithAudioActivity:self.playbackAudioActivity];
[self play];
} }
- (void)play - (void)playWithAudioActivity:(AudioActivity *)audioActivity
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
BOOL success = [OWSAudioSession.shared startAudioActivity:audioActivity];
OWSAssertDebug(success);
OWSAssertDebug(self.mediaUrl); OWSAssertDebug(self.mediaUrl);
OWSAssertDebug([self.delegate audioPlaybackState] != AudioPlaybackState_Playing); OWSAssertDebug([self.delegate audioPlaybackState] != AudioPlaybackState_Playing);
@ -157,7 +160,7 @@ NS_ASSUME_NONNULL_BEGIN
[self.audioPlayerPoller invalidate]; [self.audioPlayerPoller invalidate];
[self.delegate setAudioProgress:(CGFloat)[self.audioPlayer currentTime] duration:(CGFloat)[self.audioPlayer duration]]; [self.delegate setAudioProgress:(CGFloat)[self.audioPlayer currentTime] duration:(CGFloat)[self.audioPlayer duration]];
[OWSAudioSession.shared endAudioActivity:self.audioActivity]; [self endAudioActivities];
[DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self]; [DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self];
} }
@ -170,18 +173,24 @@ NS_ASSUME_NONNULL_BEGIN
[self.audioPlayerPoller invalidate]; [self.audioPlayerPoller invalidate];
[self.delegate setAudioProgress:0 duration:0]; [self.delegate setAudioProgress:0 duration:0];
[OWSAudioSession.shared endAudioActivity:self.audioActivity]; [self endAudioActivities];
[DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self]; [DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self];
} }
- (void)togglePlayState - (void)endAudioActivities
{
[OWSAudioSession.shared endAudioActivity:self.playbackAudioActivity];
[OWSAudioSession.shared endAudioActivity:self.currentCategoryAudioActivity];
}
- (void)togglePlayStateWithPlaybackAudioCategory
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
if (self.delegate.audioPlaybackState == AudioPlaybackState_Playing) { if (self.delegate.audioPlaybackState == AudioPlaybackState_Playing) {
[self pause]; [self pause];
} else { } else {
[self play]; [self playWithAudioActivity:self.playbackAudioActivity];
} }
} }