2017-01-18 17:46:29 +01:00
//
2017-01-26 16:05:41 +01:00
// C o p y r i g h t ( c ) 2 0 1 7 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
2017-01-18 17:46:29 +01:00
//
import Foundation
2017-02-25 02:33:43 +01:00
import AVFoundation
2017-01-18 17:46:29 +01:00
2017-01-19 15:38:50 +01:00
@objc class CallAudioService : NSObject , CallObserver {
2017-01-18 17:46:29 +01:00
private let TAG = " [CallAudioService] "
private var vibrateTimer : Timer ?
2017-02-25 02:33:43 +01:00
private let audioPlayer = AVAudioPlayer ( )
2017-01-18 18:31:18 +01:00
private let handleRinging : Bool
2017-01-18 17:46:29 +01:00
2017-02-25 02:33:43 +01:00
class Sound {
let TAG = " [Sound] "
static let incomingRing = Sound ( filePath : " r " , fileExtension : " caf " , loop : true )
static let outgoingRing = Sound ( filePath : " outring " , fileExtension : " mp3 " , loop : true )
static let dialing = Sound ( filePath : " sonarping " , fileExtension : " mp3 " , loop : true )
static let busy = Sound ( filePath : " busy " , fileExtension : " mp3 " , loop : false )
static let failure = Sound ( filePath : " failure " , fileExtension : " mp3 " , loop : false )
let filePath : String
let fileExtension : String
let url : URL
let loop : Bool
init ( filePath : String , fileExtension : String , loop : Bool ) {
self . filePath = filePath
self . fileExtension = fileExtension
self . url = Bundle . main . url ( forResource : self . filePath , withExtension : self . fileExtension ) !
self . loop = loop
}
lazy var player : AVAudioPlayer ? = {
let newPlayer : AVAudioPlayer ?
do {
try newPlayer = AVAudioPlayer ( contentsOf : self . url , fileTypeHint : nil )
if self . loop {
newPlayer ? . numberOfLoops = - 1
}
} catch {
2017-07-10 20:52:14 +02:00
owsFail ( " \( self . TAG ) failed to build audio player with error: \( error ) " )
2017-02-25 02:33:43 +01:00
newPlayer = nil
}
return newPlayer
} ( )
2017-01-18 17:46:29 +01:00
}
2017-01-18 18:31:18 +01:00
// MARK: V i b r a t i o n c o n f i g
2017-01-18 17:46:29 +01:00
private let vibrateRepeatDuration = 1.6
// O u r r i n g b u z z i s a p a i r o f v i b r a t i o n s .
// ` p u l s e D u r a t i o n ` i s t h e s m a l l p a u s e b e t w e e n t h e t w o v i b r a t i o n s i n t h e p a i r .
private let pulseDuration = 0.2
2017-01-18 18:31:18 +01:00
// MARK: - I n i t i a l i z e r s
init ( handleRinging : Bool ) {
self . handleRinging = handleRinging
}
2017-01-19 15:38:50 +01:00
// MARK: - C a l l O b s e r v e r
internal func stateDidChange ( call : SignalCall , state : CallState ) {
2017-01-26 16:05:41 +01:00
AssertIsOnMainThread ( )
2017-01-31 18:41:51 +01:00
self . handleState ( call : call )
2017-01-19 15:38:50 +01:00
}
internal func muteDidChange ( call : SignalCall , isMuted : Bool ) {
2017-01-26 16:05:41 +01:00
AssertIsOnMainThread ( )
2017-01-19 15:38:50 +01:00
Logger . verbose ( " \( TAG ) in \( #function ) is no-op " )
}
internal func speakerphoneDidChange ( call : SignalCall , isEnabled : Bool ) {
2017-01-26 16:05:41 +01:00
AssertIsOnMainThread ( )
2017-01-27 17:11:33 +01:00
2017-07-03 15:42:30 +02:00
ensureProperAudioSession ( call : call )
2017-01-27 17:11:33 +01:00
}
internal func hasLocalVideoDidChange ( call : SignalCall , hasLocalVideo : Bool ) {
AssertIsOnMainThread ( )
2017-07-03 15:42:30 +02:00
ensureProperAudioSession ( call : call )
2017-01-27 17:11:33 +01:00
}
2017-07-03 15:42:30 +02:00
private func ensureProperAudioSession ( call : SignalCall ? ) {
2017-05-03 15:34:13 +02:00
guard let call = call else {
setAudioSession ( category : AVAudioSessionCategoryPlayback ,
mode : AVAudioSessionModeDefault )
return
}
2017-07-03 15:42:30 +02:00
if call . state = = . localRinging {
// S o l o A m b i e n t p l a y s t h r o u g h s p e a k e r , b u t r e s p e c t s s i l e n t s w i t c h
2017-07-07 00:37:25 +02:00
setAudioSession ( category : AVAudioSessionCategorySoloAmbient ,
mode : AVAudioSessionModeDefault )
2017-07-03 15:42:30 +02:00
} else if call . hasLocalVideo {
// A u t o - e n a b l e s p e a k e r p h o n e w h e n l o c a l v i d e o i s e n a b l e d .
2017-01-31 16:43:45 +01:00
setAudioSession ( category : AVAudioSessionCategoryPlayAndRecord ,
mode : AVAudioSessionModeVideoChat ,
2017-07-07 00:37:25 +02:00
options : [ . defaultToSpeaker , . allowBluetooth ] )
2017-01-31 16:43:45 +01:00
} else if call . isSpeakerphoneEnabled {
setAudioSession ( category : AVAudioSessionCategoryPlayAndRecord ,
mode : AVAudioSessionModeVoiceChat ,
2017-07-07 00:37:25 +02:00
options : [ . defaultToSpeaker , . allowBluetooth ] )
2017-01-19 15:38:50 +01:00
} else {
2017-02-03 01:34:58 +01:00
setAudioSession ( category : AVAudioSessionCategoryPlayAndRecord ,
2017-07-07 00:37:25 +02:00
mode : AVAudioSessionModeVoiceChat ,
options : [ . allowBluetooth ] )
2017-01-19 15:38:50 +01:00
}
}
2017-01-18 18:31:18 +01:00
// MARK: - S e r v i c e a c t i o n h a n d l e r s
2017-05-03 15:34:13 +02:00
public func didUpdateVideoTracks ( call : SignalCall ? ) {
Logger . verbose ( " \( TAG ) in \( #function ) " )
2017-07-03 15:42:30 +02:00
self . ensureProperAudioSession ( call : call )
2017-05-03 15:34:13 +02:00
}
2017-01-31 18:41:51 +01:00
public func handleState ( call : SignalCall ) {
2017-01-23 22:17:55 +01:00
assert ( Thread . isMainThread )
2017-01-31 18:41:51 +01:00
Logger . verbose ( " \( TAG ) in \( #function ) new state: \( call . state ) " )
2017-01-19 15:38:50 +01:00
2017-01-31 18:41:51 +01:00
switch call . state {
2017-02-25 02:33:43 +01:00
case . idle : handleIdle ( call : call )
case . dialing : handleDialing ( call : call )
case . answering : handleAnswering ( call : call )
case . remoteRinging : handleRemoteRinging ( call : call )
case . localRinging : handleLocalRinging ( call : call )
case . connected : handleConnected ( call : call )
case . localFailure : handleLocalFailure ( call : call )
case . localHangup : handleLocalHangup ( call : call )
case . remoteHangup : handleRemoteHangup ( call : call )
case . remoteBusy : handleBusy ( call : call )
2017-01-18 17:46:29 +01:00
}
}
2017-02-25 02:33:43 +01:00
private func handleIdle ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
}
2017-02-25 02:33:43 +01:00
private func handleDialing ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
2017-07-03 15:42:30 +02:00
ensureProperAudioSession ( call : call )
2017-02-25 02:33:43 +01:00
// H A C K : W i t h o u t t h i s a s y n c , d i a l i n g s o u n d o n l y p l a y s o n c e . I d o n ' t r e a l l y u n d e r s t a n d w h y . D o e s t h e a u d i o S e s s i o n
// n e e d s o m e t i m e t o s e t t l e ? I s s o m e t h i g n e l s e i n t e r r u p t i n g o u r s e s s i o n ?
DispatchQueue . main . asyncAfter ( deadline : DispatchTime . now ( ) + 0.2 ) {
2017-07-03 15:42:30 +02:00
self . play ( sound : Sound . dialing )
2017-02-25 02:33:43 +01:00
}
2017-01-18 17:46:29 +01:00
}
2017-02-25 02:33:43 +01:00
private func handleAnswering ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
stopPlayingAnySounds ( )
2017-07-03 15:42:30 +02:00
self . ensureProperAudioSession ( call : call )
2017-01-18 17:46:29 +01:00
}
2017-02-25 02:33:43 +01:00
private func handleRemoteRinging ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
stopPlayingAnySounds ( )
// F I X M E i f y o u t o g g l e d s p e a k e r p h o n e b e f o r e t h i s p o i n t , t h e o u t g o i n g r i n g d o e s n o t p l a y t h r o u g h s p e a k e r . W h y ?
2017-07-03 15:42:30 +02:00
self . play ( sound : Sound . outgoingRing )
2017-01-18 17:46:29 +01:00
}
2017-02-25 02:33:43 +01:00
private func handleLocalRinging ( call : SignalCall ) {
2017-01-18 18:31:18 +01:00
Logger . debug ( " \( TAG ) in \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
2017-07-03 15:42:30 +02:00
stopPlayingAnySounds ( )
ensureProperAudioSession ( call : call )
2017-02-25 02:33:43 +01:00
startRinging ( call : call )
2017-01-18 17:46:29 +01:00
}
2017-01-31 18:41:51 +01:00
private func handleConnected ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
stopPlayingAnySounds ( )
2017-01-18 17:46:29 +01:00
2017-02-25 02:33:43 +01:00
// s t a r t r e c o r d i n g t o t r a n s m i t c a l l a u d i o .
2017-07-03 15:42:30 +02:00
ensureProperAudioSession ( call : call )
2017-01-18 17:46:29 +01:00
}
2017-02-25 02:33:43 +01:00
private func handleLocalFailure ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
stopPlayingAnySounds ( )
2017-07-03 15:42:30 +02:00
play ( sound : Sound . failure )
2017-01-18 17:46:29 +01:00
}
2017-02-25 02:33:43 +01:00
private func handleLocalHangup ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
handleCallEnded ( call : call )
2017-01-18 17:46:29 +01:00
}
2017-02-25 02:33:43 +01:00
private func handleRemoteHangup ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
vibrate ( )
2017-02-27 20:46:19 +01:00
handleCallEnded ( call : call )
2017-01-18 17:46:29 +01:00
}
2017-02-25 02:33:43 +01:00
private func handleBusy ( call : SignalCall ) {
2017-01-18 17:46:29 +01:00
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
stopPlayingAnySounds ( )
2017-07-03 15:42:30 +02:00
play ( sound : Sound . busy )
2017-02-25 02:33:43 +01:00
// L e t t h e b u s y s o u n d p l a y f o r 4 s e c o n d s . T h e f u l l f i l e i s l o n g e r t h a n n e c e s s a r y
DispatchQueue . main . asyncAfter ( deadline : DispatchTime . now ( ) + 4.0 ) {
2017-02-27 20:46:19 +01:00
self . handleCallEnded ( call : call )
2017-02-25 02:33:43 +01:00
}
}
private func handleCallEnded ( call : SignalCall ) {
Logger . debug ( " \( TAG ) \( #function ) " )
2017-07-03 15:42:30 +02:00
AssertIsOnMainThread ( )
2017-02-25 02:33:43 +01:00
stopPlayingAnySounds ( )
// S t o p s o l o a u d i o , r e v e r t t o d e f a u l t .
setAudioSession ( category : AVAudioSessionCategoryAmbient )
}
// MARK: P l a y i n g S o u n d s
var currentPlayer : AVAudioPlayer ?
private func stopPlayingAnySounds ( ) {
currentPlayer ? . stop ( )
stopAnyRingingVibration ( )
}
2017-07-03 15:42:30 +02:00
private func play ( sound : Sound ) {
2017-02-25 02:33:43 +01:00
guard let newPlayer = sound . player else {
2017-07-10 20:52:14 +02:00
owsFail ( " \( self . TAG ) unable to build player " )
2017-02-25 02:33:43 +01:00
return
}
Logger . info ( " \( self . TAG ) playing sound: \( sound . filePath ) " )
2017-07-05 18:55:00 +02:00
// I t ' s i m p o r t a n t t o s t o p t h e c u r r e n t p l a y e r * * b e f o r e * * s t a r t i n g t h e n e w p l a y e r . I n t h e c a s e t h a t
// w e ' r e p l a y i n g t h e s a m e s o u n d , s i n c e t h e p l a y e r i s m e m o i z e d o n t h e s o u n d i n s t a n c e , w e ' d o t h e r w i s e
// s t o p t h e s o u n d w e j u s t s t a r t e d .
2017-02-25 02:33:43 +01:00
self . currentPlayer ? . stop ( )
2017-07-03 19:57:54 +02:00
newPlayer . play ( )
2017-02-25 02:33:43 +01:00
self . currentPlayer = newPlayer
2017-01-18 17:46:29 +01:00
}
2017-01-18 18:31:18 +01:00
// MARK: - R i n g i n g
2017-02-25 02:33:43 +01:00
private func startRinging ( call : SignalCall ) {
2017-01-18 18:31:18 +01:00
guard handleRinging else {
Logger . debug ( " \( TAG ) ignoring \( #function ) since CallKit handles it's own ringing state " )
return
}
2017-01-26 16:05:41 +01:00
vibrateTimer = WeakTimer . scheduledTimer ( timeInterval : vibrateRepeatDuration , target : self , userInfo : nil , repeats : true ) { [ weak self ] _ in
2017-01-19 16:57:07 +01:00
self ? . ringVibration ( )
}
vibrateTimer ? . fire ( )
2017-07-03 15:42:30 +02:00
play ( sound : Sound . incomingRing )
2017-01-18 18:31:18 +01:00
}
2017-01-18 17:46:29 +01:00
2017-02-25 02:33:43 +01:00
private func stopAnyRingingVibration ( ) {
2017-01-18 18:31:18 +01:00
guard handleRinging else {
Logger . debug ( " \( TAG ) ignoring \( #function ) since CallKit handles it's own ringing state " )
return
}
Logger . debug ( " \( TAG ) in \( #function ) " )
// S t o p v i b r a t i n g
2017-01-18 17:46:29 +01:00
vibrateTimer ? . invalidate ( )
vibrateTimer = nil
}
// p u b l i c s o i t c a n b e c a l l e d b y t i m e r v i a s e l e c t o r
public func ringVibration ( ) {
// S i n c e a c a l l n o t i f i c a t i o n i s m o r e u r g e n t t h a n a m e s s a g e n o t i f a c t i o n , w e
// v i b r a t e t w i c e , l i k e a p u l s e , t o d i f f e r e n t i a t e f r o m a n o r m a l n o t i f i c a t i o n v i b r a t i o n .
2017-02-25 02:33:43 +01:00
vibrate ( )
2017-01-18 17:46:29 +01:00
DispatchQueue . default . asyncAfter ( deadline : DispatchTime . now ( ) + pulseDuration ) {
2017-02-25 02:33:43 +01:00
self . vibrate ( )
2017-01-18 17:46:29 +01:00
}
}
2017-02-25 02:33:43 +01:00
func vibrate ( ) {
// T O D O i m p l e m e n t H a p t i c A d a p t e r f o r i P h o n e 7 a n d u p
2017-02-27 20:46:19 +01:00
AudioServicesPlaySystemSound ( kSystemSoundID_Vibrate )
2017-02-25 02:33:43 +01:00
}
2017-01-31 16:43:45 +01:00
private func setAudioSession ( category : String ,
2017-02-03 01:34:58 +01:00
mode : String ? = nil ,
options : AVAudioSessionCategoryOptions = AVAudioSessionCategoryOptions ( rawValue : 0 ) ) {
2017-07-03 15:42:30 +02:00
let session = AVAudioSession . sharedInstance ( )
2017-01-18 17:46:29 +01:00
do {
2017-02-03 01:34:58 +01:00
if #available ( iOS 10.0 , * ) , let mode = mode {
2017-07-07 00:37:25 +02:00
let oldCategory = session . category
let oldMode = session . mode
let oldOptions = session . categoryOptions
2017-07-08 23:43:34 +02:00
guard oldCategory != category || oldMode != mode || oldOptions != options else {
2017-07-03 15:42:30 +02:00
return
}
2017-07-07 00:37:25 +02:00
if oldCategory != category {
Logger . debug ( " \( self . TAG ) audio session changed category: \( oldCategory ) -> \( category ) " )
}
if oldMode != mode {
Logger . debug ( " \( self . TAG ) audio session changed mode: \( oldMode ) -> \( mode ) " )
}
if oldOptions != options {
2017-07-08 23:43:34 +02:00
Logger . debug ( " \( self . TAG ) audio session changed options: \( oldOptions ) -> \( options ) " )
2017-07-07 00:37:25 +02:00
}
2017-07-03 15:42:30 +02:00
try session . setCategory ( category , mode : mode , options : options )
2017-07-07 00:37:25 +02:00
2017-01-31 16:43:45 +01:00
} else {
2017-07-07 00:37:25 +02:00
let oldCategory = session . category
let oldOptions = session . categoryOptions
2017-07-08 23:43:34 +02:00
guard session . category != category || session . categoryOptions != options else {
2017-07-03 15:42:30 +02:00
return
}
2017-07-07 00:37:25 +02:00
if oldCategory != category {
Logger . debug ( " \( self . TAG ) audio session changed category: \( oldCategory ) -> \( category ) " )
}
if oldOptions != options {
2017-07-08 23:43:34 +02:00
Logger . debug ( " \( self . TAG ) audio session changed options: \( oldOptions ) -> \( options ) " )
2017-07-07 00:37:25 +02:00
}
2017-07-03 15:42:30 +02:00
try session . setCategory ( category , with : options )
2017-07-07 00:37:25 +02:00
2017-01-31 16:43:45 +01:00
}
2017-01-18 17:46:29 +01:00
} catch {
2017-05-05 00:17:18 +02:00
let message = " \( self . TAG ) in \( #function ) failed to set category: \( category ) mode: \( String ( describing : mode ) ) , options: \( options ) with error: \( error ) "
2017-07-10 20:52:14 +02:00
owsFail ( message )
2017-01-18 17:46:29 +01:00
}
}
}