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, 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

View File

@ -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()

View File

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

View File

@ -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")

View File

@ -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<AudioActivity>] = []
// 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()
}

View File

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

View File

@ -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];
}
}