diff --git a/Custom-Audio-Driver/Custom-Audio-Driver/DefaultAudioDevice.swift b/Custom-Audio-Driver/Custom-Audio-Driver/DefaultAudioDevice.swift index 0a83814..6bc51bc 100644 --- a/Custom-Audio-Driver/Custom-Audio-Driver/DefaultAudioDevice.swift +++ b/Custom-Audio-Driver/Custom-Audio-Driver/DefaultAudioDevice.swift @@ -293,6 +293,7 @@ extension DefaultAudioDevice: OTAudioDevice { return recording } func estimatedRenderDelay() -> UInt16 { + //No problem with casting. plaoutDelay will never be bigger than 1000 return UInt16(playoutDelay) } func estimatedCaptureDelay() -> UInt16 { @@ -630,20 +631,29 @@ func updatePlayoutDelay(withAudioDevice audioDevice: DefaultAudioDevice) { audioDevice.playoutDelayMeasurementCounter += 1 if audioDevice.playoutDelayMeasurementCounter >= 100 { // Update HW and OS delay every second, unlikely to change - audioDevice.playoutDelay = 0 + var tempPlayoutDelay: UInt32 = 0 let session = AVAudioSession.sharedInstance() // HW output latency let interval = session.outputLatency - audioDevice.playoutDelay += UInt32(interval * 1000000) + tempPlayoutDelay += UInt32(interval * 1000000) // HW buffer duration let ioInterval = session.ioBufferDuration - audioDevice.playoutDelay += UInt32(ioInterval * 1000000) - audioDevice.playoutDelay += UInt32(audioDevice.playoutAudioUnitPropertyLatency * 1000000) + tempPlayoutDelay += UInt32(ioInterval * 1000000) + tempPlayoutDelay += UInt32(audioDevice.playoutAudioUnitPropertyLatency * 1000000) // To ms - audioDevice.playoutDelay = (audioDevice.playoutDelay - 500) / 1000 - - audioDevice.playoutDelayMeasurementCounter = 0 + tempPlayoutDelay = (audioDevice.playoutDelay - 500) / 1000 + + //playout valid interval [0 - 1000]ms + if(tempPlayoutDelay > 1000) + { + //input parameter error + audioDevice.playoutDelayMeasurementCounter = 100 + } else + { + audioDevice.playoutDelay = tempPlayoutDelay + audioDevice.playoutDelayMeasurementCounter = 0 + } } } diff --git a/Simple-Multiparty/Podfile.lock b/Simple-Multiparty/Podfile.lock index 37b25de..d967001 100644 --- a/Simple-Multiparty/Podfile.lock +++ b/Simple-Multiparty/Podfile.lock @@ -13,4 +13,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: ec3904194bdf3baef5bcfd4f642012ed6f5538c5 -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.3 diff --git a/Simple-Multiparty/Simple-Multiparty.xcodeproj/project.pbxproj b/Simple-Multiparty/Simple-Multiparty.xcodeproj/project.pbxproj index d0d7a07..2b2f283 100644 --- a/Simple-Multiparty/Simple-Multiparty.xcodeproj/project.pbxproj +++ b/Simple-Multiparty/Simple-Multiparty.xcodeproj/project.pbxproj @@ -7,20 +7,26 @@ objects = { /* Begin PBXBuildFile section */ + 2CF52467150362DAB76ED36F /* Pods_Simple_Multiparty.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A69F800D6C9AF74C79D9C435 /* Pods_Simple_Multiparty.framework */; }; A05375F91EB1637B00645696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375F11EB1637B00645696 /* AppDelegate.swift */; }; A05375FA1EB1637B00645696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A05375F21EB1637B00645696 /* Assets.xcassets */; }; A05375FB1EB1637B00645696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375F31EB1637B00645696 /* LaunchScreen.storyboard */; }; A05375FC1EB1637B00645696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375F51EB1637B00645696 /* Main.storyboard */; }; A05375FE1EB1637B00645696 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375F81EB1637B00645696 /* ViewController.swift */; }; + AD96BCE728D33B8D007C8AE5 /* DefaultAudioDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD96BCE628D33B8D007C8AE5 /* DefaultAudioDevice.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 38A96086E7512E399DFF2ED7 /* Pods-Simple-Multiparty.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Simple-Multiparty.debug.xcconfig"; path = "Target Support Files/Pods-Simple-Multiparty/Pods-Simple-Multiparty.debug.xcconfig"; sourceTree = ""; }; + 9E6A4CFD4E6D554D154FEF4E /* Pods-Simple-Multiparty.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Simple-Multiparty.release.xcconfig"; path = "Target Support Files/Pods-Simple-Multiparty/Pods-Simple-Multiparty.release.xcconfig"; sourceTree = ""; }; A05375F11EB1637B00645696 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A05375F21EB1637B00645696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A05375F41EB1637B00645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A05375F61EB1637B00645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A05375F71EB1637B00645696 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A05375F81EB1637B00645696 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + A69F800D6C9AF74C79D9C435 /* Pods_Simple_Multiparty.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Simple_Multiparty.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AD96BCE628D33B8D007C8AE5 /* DefaultAudioDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAudioDevice.swift; sourceTree = ""; }; F829C8E51D9AB57700CDFBD5 /* Simple-Multiparty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Simple-Multiparty.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -29,12 +35,30 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2CF52467150362DAB76ED36F /* Pods_Simple_Multiparty.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 095AFCE1F560C836B338FFFC /* Frameworks */ = { + isa = PBXGroup; + children = ( + A69F800D6C9AF74C79D9C435 /* Pods_Simple_Multiparty.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 86D18B3A61C2DCD118F00945 /* Pods */ = { + isa = PBXGroup; + children = ( + 38A96086E7512E399DFF2ED7 /* Pods-Simple-Multiparty.debug.xcconfig */, + 9E6A4CFD4E6D554D154FEF4E /* Pods-Simple-Multiparty.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; A05375F01EB1637B00645696 /* Simple-Multiparty */ = { isa = PBXGroup; children = ( @@ -44,6 +68,7 @@ A05375F51EB1637B00645696 /* Main.storyboard */, A05375F71EB1637B00645696 /* Info.plist */, A05375F81EB1637B00645696 /* ViewController.swift */, + AD96BCE628D33B8D007C8AE5 /* DefaultAudioDevice.swift */, ); path = "Simple-Multiparty"; sourceTree = ""; @@ -53,6 +78,8 @@ children = ( A05375F01EB1637B00645696 /* Simple-Multiparty */, F829C8E61D9AB57700CDFBD5 /* Products */, + 86D18B3A61C2DCD118F00945 /* Pods */, + 095AFCE1F560C836B338FFFC /* Frameworks */, ); sourceTree = ""; }; @@ -71,6 +98,7 @@ isa = PBXNativeTarget; buildConfigurationList = F829C8F71D9AB57700CDFBD5 /* Build configuration list for PBXNativeTarget "Simple-Multiparty" */; buildPhases = ( + 58E41DACD660DB613732DD44 /* [CP] Check Pods Manifest.lock */, F829C8E11D9AB57700CDFBD5 /* Sources */, F829C8E21D9AB57700CDFBD5 /* Frameworks */, F829C8E31D9AB57700CDFBD5 /* Resources */, @@ -133,12 +161,38 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 58E41DACD660DB613732DD44 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Simple-Multiparty-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ F829C8E11D9AB57700CDFBD5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375FE1EB1637B00645696 /* ViewController.swift in Sources */, + AD96BCE728D33B8D007C8AE5 /* DefaultAudioDevice.swift in Sources */, A05375F91EB1637B00645696 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -278,6 +332,7 @@ }; F829C8F81D9AB57700CDFBD5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 38A96086E7512E399DFF2ED7 /* Pods-Simple-Multiparty.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; @@ -292,6 +347,7 @@ }; F829C8F91D9AB57700CDFBD5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9E6A4CFD4E6D554D154FEF4E /* Pods-Simple-Multiparty.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; diff --git a/Simple-Multiparty/Simple-Multiparty/DefaultAudioDevice.swift b/Simple-Multiparty/Simple-Multiparty/DefaultAudioDevice.swift new file mode 100644 index 0000000..deae7ae --- /dev/null +++ b/Simple-Multiparty/Simple-Multiparty/DefaultAudioDevice.swift @@ -0,0 +1,671 @@ +// +// DefaultAudioDevice.swift +// 4.Custom-Audio-Driver +// +// Created by Roberto Perez Cubero on 21/09/2016. +// Copyright © 2016 tokbox. All rights reserved. +// + +import Foundation +import OpenTok + +class DefaultAudioDevice: NSObject { +#if targetEnvironment(simulator) + static let kSampleRate: UInt16 = 44100 +#else + static let kSampleRate: UInt16 = 48000 +#endif + static let kOutputBus = AudioUnitElement(0) + static let kInputBus = AudioUnitElement(1) + static let kAudioDeviceHeadset = "AudioSessionManagerDevice_Headset" + static let kAudioDeviceBluetooth = "AudioSessionManagerDevice_Bluetooth" + static let kAudioDeviceSpeaker = "AudioSessionManagerDevice_Speaker" + + var audioFormat = OTAudioFormat() + let safetyQueue = DispatchQueue(label: "ot-audio-driver") + + var deviceAudioBus: OTAudioBus? + + func setAudioBus(_ audioBus: OTAudioBus?) -> Bool { + deviceAudioBus = audioBus + audioFormat = OTAudioFormat() + audioFormat.sampleRate = DefaultAudioDevice.kSampleRate + audioFormat.numChannels = 1 + return true + } + + var bufferList: UnsafeMutablePointer? + var bufferSize: UInt32 = 0 + var bufferNumFrames: UInt32 = 0 + var playoutAudioUnitPropertyLatency: Float64 = 0 + var playoutDelayMeasurementCounter: UInt32 = 0 + var recordingDelayMeasurementCounter: UInt32 = 0 + var recordingDelayHWAndOS: UInt32 = 0 + var recordingDelay: UInt32 = 0 + var recordingAudioUnitPropertyLatency: Float64 = 0 + var playoutDelay: UInt32 = 0 + var playing = false + var playoutInitialized = false + var recording = false + var recordingInitialized = false + var interruptedPlayback = false + var isRecorderInterrupted = false + var isPlayerInterrupted = false + var isResetting = false + var restartRetryCount = 0 + fileprivate var recordingVoiceUnit: AudioUnit? + fileprivate var playoutVoiceUnit: AudioUnit? + + fileprivate var previousAVAudioSessionCategory: AVAudioSession.Category? + fileprivate var avAudioSessionMode: AVAudioSession.Mode? + fileprivate var avAudioSessionPreffSampleRate = Double(0) + fileprivate var avAudioSessionChannels = 0 + fileprivate var isAudioSessionSetup = false + + var areListenerBlocksSetup = false + var streamFormat = AudioStreamBasicDescription() + + override init() { + audioFormat.sampleRate = DefaultAudioDevice.kSampleRate + audioFormat.numChannels = 1 + } + + deinit { + tearDownAudio() + removeObservers() + } + + + fileprivate func restartAudio() { + safetyQueue.async { + self.doRestartAudio(numberOfAttempts: 3) + } + } + + fileprivate func restartAudioAfterInterruption() { + if isRecorderInterrupted { + if startCapture() { + isRecorderInterrupted = false + restartRetryCount = 0 + } else { + restartRetryCount += 1 + if restartRetryCount < 3 { + safetyQueue.asyncAfter(deadline: DispatchTime.now(), execute: { [unowned self] in + self.restartAudioAfterInterruption() + }) + } else { + isRecorderInterrupted = false + isPlayerInterrupted = false + restartRetryCount = 0 + print("ERROR[OpenTok]:Unable to acquire audio session") + } + } + } + if isPlayerInterrupted { + isPlayerInterrupted = false + let _ = startRendering() + } + } + + fileprivate func doRestartAudio(numberOfAttempts: Int) { + + if recording { + let _ = stopCapture() + disposeAudioUnit(audioUnit: &recordingVoiceUnit) + let _ = startCapture() + } + + if playing { + let _ = self.stopRendering() + disposeAudioUnit(audioUnit: &playoutVoiceUnit) + let _ = self.startRendering() + } + isResetting = false + } + + fileprivate func setupAudioUnit(withPlayout playout: Bool) -> Bool { + if !isAudioSessionSetup { + setupAudioSession() + isAudioSessionSetup = true + } + + let bytesPerSample = UInt32(MemoryLayout.size) + streamFormat.mFormatID = kAudioFormatLinearPCM + streamFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked + streamFormat.mBytesPerPacket = bytesPerSample + streamFormat.mFramesPerPacket = 1 + streamFormat.mBytesPerFrame = bytesPerSample + streamFormat.mChannelsPerFrame = 1 + streamFormat.mBitsPerChannel = 8 * bytesPerSample + streamFormat.mSampleRate = Float64(DefaultAudioDevice.kSampleRate) + + var audioUnitDescription = AudioComponentDescription() + audioUnitDescription.componentType = kAudioUnitType_Output + audioUnitDescription.componentSubType = kAudioUnitSubType_VoiceProcessingIO + audioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple + audioUnitDescription.componentFlags = 0 + audioUnitDescription.componentFlagsMask = 0 + + let foundVpioUnitRef = AudioComponentFindNext(nil, &audioUnitDescription) + let result: OSStatus = { + if playout { + return AudioComponentInstanceNew(foundVpioUnitRef!, &playoutVoiceUnit) + } else { + return AudioComponentInstanceNew(foundVpioUnitRef!, &recordingVoiceUnit) + } + }() + + if result != noErr { + print("Error seting up audio unit") + return false + } + + var value: UInt32 = 1 + if playout { + AudioUnitSetProperty(playoutVoiceUnit!, kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, DefaultAudioDevice.kOutputBus, &value, + UInt32(MemoryLayout.size)) + + AudioUnitSetProperty(playoutVoiceUnit!, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, DefaultAudioDevice.kOutputBus, &streamFormat, + UInt32(MemoryLayout.size)) + // Disable Input on playout + var enableInput = 0 + AudioUnitSetProperty(playoutVoiceUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, + DefaultAudioDevice.kInputBus, &enableInput, UInt32(MemoryLayout.size)) + } else { + AudioUnitSetProperty(recordingVoiceUnit!, kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Input, DefaultAudioDevice.kInputBus, &value, + UInt32(MemoryLayout.size)) + AudioUnitSetProperty(recordingVoiceUnit!, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, DefaultAudioDevice.kInputBus, &streamFormat, + UInt32(MemoryLayout.size)) + // Disable Output on record + var enableOutput = 0 + AudioUnitSetProperty(recordingVoiceUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, + DefaultAudioDevice.kOutputBus, &enableOutput, UInt32(MemoryLayout.size)) + } + + if playout { + setupPlayoutCallback() + } else { + setupRecordingCallback() + } + + setBluetoothAsPreferredInputDevice() + + return true + } + + fileprivate func setupPlayoutCallback() { + let selfPointer = Unmanaged.passUnretained(self).toOpaque() + var renderCallback = AURenderCallbackStruct(inputProc: renderCb, inputProcRefCon: selfPointer) + AudioUnitSetProperty(playoutVoiceUnit!, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + DefaultAudioDevice.kOutputBus, + &renderCallback, + UInt32(MemoryLayout.size)) + + } + + fileprivate func setupRecordingCallback() { + let selfPointer = Unmanaged.passUnretained(self).toOpaque() + var inputCallback = AURenderCallbackStruct(inputProc: recordCb, inputProcRefCon: selfPointer) + AudioUnitSetProperty(recordingVoiceUnit!, + kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + DefaultAudioDevice.kInputBus, + &inputCallback, + UInt32(MemoryLayout.size)) + + var value = 0 + AudioUnitSetProperty(recordingVoiceUnit!, + kAudioUnitProperty_ShouldAllocateBuffer, + kAudioUnitScope_Output, + DefaultAudioDevice.kInputBus, + &value, + UInt32(MemoryLayout.size)) + } + + fileprivate func disposeAudioUnit(audioUnit: inout AudioUnit?) { + if let unit = audioUnit { + AudioUnitUninitialize(unit) + AudioComponentInstanceDispose(unit) + } + audioUnit = nil + } + + fileprivate func tearDownAudio() { + print("Destoying audio units") + disposeAudioUnit(audioUnit: &playoutVoiceUnit) + disposeAudioUnit(audioUnit: &recordingVoiceUnit) + freeupAudioBuffers() + + let session = AVAudioSession.sharedInstance() + do { + guard let previousAVAudioSessionCategory = previousAVAudioSessionCategory else { return } + try session.setCategory(previousAVAudioSessionCategory, mode: .default) + guard let avAudioSessionMode = avAudioSessionMode else { return } + try session.setMode(avAudioSessionMode) + try session.setPreferredSampleRate(avAudioSessionPreffSampleRate) + try session.setPreferredInputNumberOfChannels(avAudioSessionChannels) + + isAudioSessionSetup = false + } catch { + print("Error reseting AVAudioSession") + } + } + + fileprivate func freeupAudioBuffers() { + if var data = bufferList?.pointee, data.mBuffers.mData != nil { + data.mBuffers.mData?.assumingMemoryBound(to: UInt16.self).deallocate() + data.mBuffers.mData = nil + } + + if let list = bufferList { + list.deallocate() + } + + bufferList = nil + bufferNumFrames = 0 + } +} + +// MARK: - Audio Device Implementation +extension DefaultAudioDevice: OTAudioDevice { + func captureFormat() -> OTAudioFormat { + return audioFormat + } + func renderFormat() -> OTAudioFormat { + return audioFormat + } + func renderingIsAvailable() -> Bool { + return true + } + func renderingIsInitialized() -> Bool { + return playoutInitialized + } + func isRendering() -> Bool { + return playing + } + func isCapturing() -> Bool { + return recording + } + func estimatedRenderDelay() -> UInt16 { + print("playoutDelay \(playoutDelay)") + return UInt16(playoutDelay) + } + func estimatedCaptureDelay() -> UInt16 { + return UInt16(recordingDelay) + } + func captureIsAvailable() -> Bool { + return true + } + func captureIsInitialized() -> Bool { + return recordingInitialized + } + + func initializeRendering() -> Bool { + if playing { return false } + + playoutInitialized = true + return playoutInitialized + } + + func startRendering() -> Bool { + if playing { return true } + playing = true + if playoutVoiceUnit == nil { + playing = setupAudioUnit(withPlayout: true) + if !playing { + return false + } + } + + let result = AudioOutputUnitStart(playoutVoiceUnit!) + + if result != noErr { + print("Error creaing rendering unit") + playing = false + } + return playing + } + + func stopRendering() -> Bool { + if !playing { + return true + } + + playing = false + + let result = AudioOutputUnitStop(playoutVoiceUnit!) + if result != noErr { + print("Error creaing playout unit") + return false + } + + if !recording && !isPlayerInterrupted && !isResetting { + tearDownAudio() + } + + return true + } + + + func initializeCapture() -> Bool { + if recording { return false } + + recordingInitialized = true + return recordingInitialized + } + + func startCapture() -> Bool { + if recording { + return true + } + + recording = true + + if recordingVoiceUnit == nil { + recording = setupAudioUnit(withPlayout: false) + + if !recording { + return false + } + } + + let result = AudioOutputUnitStart(recordingVoiceUnit!) + if result != noErr { + recording = false + } + + return recording + } + + func stopCapture() -> Bool { + if !recording { + return true + } + + recording = false + + let result = AudioOutputUnitStop(recordingVoiceUnit!) + + if result != noErr { + return false + } + + freeupAudioBuffers() + + if !recording && !isRecorderInterrupted && !isResetting { + tearDownAudio() + } + + return true + } + +} + +// MARK: - AVAudioSession +extension DefaultAudioDevice { + @objc func onInterruptionEvent(notification: Notification) { + let type = notification.userInfo?[AVAudioSessionInterruptionTypeKey] + safetyQueue.async { + self.handleInterruptionEvent(type: type as? Int) + } + } + + fileprivate func handleInterruptionEvent(type: Int?) { + guard let interruptionType = type else { + return + } + + switch UInt(interruptionType) { + case AVAudioSession.InterruptionType.began.rawValue: + if recording { + isRecorderInterrupted = true + let _ = stopCapture() + } + if playing { + isPlayerInterrupted = true + let _ = stopRendering() + } + case AVAudioSession.InterruptionType.ended.rawValue: + configureAudioSessionWithDesiredAudioRoute(desiredAudioRoute: DefaultAudioDevice.kAudioDeviceBluetooth) + restartAudioAfterInterruption() + default: + break + } + } + + @objc func onRouteChangeEvent(notification: Notification) { + safetyQueue.async { + self.handleRouteChangeEvent(notification: notification) + } + } + + @objc func appDidBecomeActive(notification: Notification) { + safetyQueue.async { + self.handleInterruptionEvent(type: Int(AVAudioSession.InterruptionType.ended.rawValue)) + } + } + + fileprivate func handleRouteChangeEvent(notification: Notification) { + guard let reason = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt else { + return + } + + if reason == AVAudioSession.RouteChangeReason.routeConfigurationChange.rawValue { + return + } + + if reason == AVAudioSession.RouteChangeReason.override.rawValue || + reason == AVAudioSession.RouteChangeReason.categoryChange.rawValue { + + let oldRouteDesc = notification.userInfo?[AVAudioSessionRouteChangePreviousRouteKey] as! AVAudioSessionRouteDescription + let outputs = oldRouteDesc.outputs + var oldOutputDeviceName: String? = nil + var currentOutputDeviceName: String? = nil + + if outputs.count > 0 { + let portDesc = outputs[0] + oldOutputDeviceName = portDesc.portName + } + + if AVAudioSession.sharedInstance().currentRoute.outputs.count > 0 { + currentOutputDeviceName = AVAudioSession.sharedInstance().currentRoute.outputs[0].portName + } + + if oldOutputDeviceName == currentOutputDeviceName || currentOutputDeviceName == nil || oldOutputDeviceName == nil { + return + } + + restartAudio() + } + } + + fileprivate func setupListenerBlocks() { + if areListenerBlocksSetup { + return + } + + let notificationCenter = NotificationCenter.default + + notificationCenter.addObserver(self, selector: #selector(DefaultAudioDevice.onInterruptionEvent), + name: AVAudioSession.interruptionNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(DefaultAudioDevice.onRouteChangeEvent(notification:)), + name: AVAudioSession.routeChangeNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(DefaultAudioDevice.appDidBecomeActive(notification:)), + name: UIApplication.didBecomeActiveNotification, object: nil) + + areListenerBlocksSetup = true + } + + fileprivate func removeObservers() { + NotificationCenter.default.removeObserver(self) + areListenerBlocksSetup = false + } + + fileprivate func setupAudioSession() { + let session = AVAudioSession.sharedInstance() + + previousAVAudioSessionCategory = session.category + avAudioSessionMode = session.mode + avAudioSessionPreffSampleRate = session.preferredSampleRate + avAudioSessionChannels = session.inputNumberOfChannels + do { + try session.setPreferredSampleRate(Double(DefaultAudioDevice.kSampleRate)) + try session.setPreferredIOBufferDuration(0.01) + let audioOptions = AVAudioSession.CategoryOptions.mixWithOthers.rawValue | + AVAudioSession.CategoryOptions.allowBluetooth.rawValue | + AVAudioSession.CategoryOptions.defaultToSpeaker.rawValue + try session.setCategory(AVAudioSession.Category.playAndRecord, mode: AVAudioSession.Mode.videoChat, options: AVAudioSession.CategoryOptions(rawValue: audioOptions)) + setupListenerBlocks() + + try session.setActive(true) + } catch let err as NSError { + print("Error setting up audio session \(err)") + } catch { + print("Error setting up audio session") + } + } +} + +// MARK: - Audio Route functions +extension DefaultAudioDevice { + fileprivate func setBluetoothAsPreferredInputDevice() { + let btRoutes = [AVAudioSession.Port.bluetoothA2DP, AVAudioSession.Port.bluetoothLE, AVAudioSession.Port.bluetoothHFP] + AVAudioSession.sharedInstance().availableInputs?.forEach({ el in + if btRoutes.contains(el.portType) { + do { + try AVAudioSession.sharedInstance().setPreferredInput(el) + } catch { + print("Error setting BT as preferred input device") + } + } + }) + } + + fileprivate func configureAudioSessionWithDesiredAudioRoute(desiredAudioRoute: String) { + let session = AVAudioSession.sharedInstance() + + if desiredAudioRoute == DefaultAudioDevice.kAudioDeviceBluetooth { + setBluetoothAsPreferredInputDevice() + } + do { + if desiredAudioRoute == DefaultAudioDevice.kAudioDeviceSpeaker { + try session.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) + } else { + try session.overrideOutputAudioPort(AVAudioSession.PortOverride.none) + } + } catch let err as NSError { + print("Error setting audio route: \(err)") + } + } +} + +// MARK: - Render and Record C Callbacks +func renderCb(inRefCon:UnsafeMutableRawPointer, + ioActionFlags:UnsafeMutablePointer, + inTimeStamp:UnsafePointer, + inBusNumber:UInt32, + inNumberFrames:UInt32, + ioData:UnsafeMutablePointer?) -> OSStatus +{ + let audioDevice: DefaultAudioDevice = Unmanaged.fromOpaque(inRefCon).takeUnretainedValue() + if !audioDevice.playing { return 0 } + + let _ = audioDevice.deviceAudioBus!.readRenderData((ioData?.pointee.mBuffers.mData)!, numberOfSamples: inNumberFrames) + updatePlayoutDelay(withAudioDevice: audioDevice) + + return noErr +} + +func recordCb(inRefCon:UnsafeMutableRawPointer, + ioActionFlags:UnsafeMutablePointer, + inTimeStamp:UnsafePointer, + inBusNumber:UInt32, + inNumberFrames:UInt32, + ioData:UnsafeMutablePointer?) -> OSStatus +{ + let audioDevice: DefaultAudioDevice = Unmanaged.fromOpaque(inRefCon).takeUnretainedValue() + if audioDevice.bufferList == nil || inNumberFrames > audioDevice.bufferNumFrames { + if audioDevice.bufferList != nil { + audioDevice.bufferList!.pointee.mBuffers.mData? + .assumingMemoryBound(to: UInt16.self).deallocate() + audioDevice.bufferList?.deallocate() + } + + audioDevice.bufferList = UnsafeMutablePointer.allocate(capacity: 1) + audioDevice.bufferList?.pointee.mNumberBuffers = 1 + audioDevice.bufferList?.pointee.mBuffers.mNumberChannels = 1 + + audioDevice.bufferList?.pointee.mBuffers.mDataByteSize = inNumberFrames * UInt32(MemoryLayout.size) + audioDevice.bufferList?.pointee.mBuffers.mData = UnsafeMutableRawPointer(UnsafeMutablePointer.allocate(capacity: Int(inNumberFrames))) + audioDevice.bufferNumFrames = inNumberFrames + audioDevice.bufferSize = (audioDevice.bufferList?.pointee.mBuffers.mDataByteSize)! + } + + AudioUnitRender(audioDevice.recordingVoiceUnit!, + ioActionFlags, + inTimeStamp, + 1, + inNumberFrames, + audioDevice.bufferList!) + + if audioDevice.recording { + audioDevice.deviceAudioBus!.writeCaptureData((audioDevice.bufferList?.pointee.mBuffers.mData)!, numberOfSamples: inNumberFrames) + } + + if audioDevice.bufferSize != audioDevice.bufferList?.pointee.mBuffers.mDataByteSize { + audioDevice.bufferList?.pointee.mBuffers.mDataByteSize = audioDevice.bufferSize + } + + updateRecordingDelay(withAudioDevice: audioDevice) + + return noErr +} + +func updatePlayoutDelay(withAudioDevice audioDevice: DefaultAudioDevice) { + audioDevice.playoutDelayMeasurementCounter += 1 + if audioDevice.playoutDelayMeasurementCounter >= 100 { + // Update HW and OS delay every second, unlikely to change + audioDevice.playoutDelay = 0 + let session = AVAudioSession.sharedInstance() + + // HW output latency + let interval = session.outputLatency + audioDevice.playoutDelay += UInt32(interval * 1000000) + // HW buffer duration + let ioInterval = session.ioBufferDuration + audioDevice.playoutDelay += UInt32(ioInterval * 1000000) + audioDevice.playoutDelay += UInt32(audioDevice.playoutAudioUnitPropertyLatency * 1000000) + // To ms + audioDevice.playoutDelay = (audioDevice.playoutDelay - 500) / 1000 + + audioDevice.playoutDelayMeasurementCounter = 0 + } +} + +func updateRecordingDelay(withAudioDevice audioDevice: DefaultAudioDevice) { + audioDevice.recordingDelayMeasurementCounter += 1 + + if audioDevice.recordingDelayMeasurementCounter >= 100 { + audioDevice.recordingDelayHWAndOS = 0 + let session = AVAudioSession.sharedInstance() + let interval = session.inputLatency + + audioDevice.recordingDelayHWAndOS += UInt32(interval * 1000000) + let ioInterval = session.ioBufferDuration + + audioDevice.recordingDelayHWAndOS += UInt32(ioInterval * 1000000) + audioDevice.recordingDelayHWAndOS += UInt32(audioDevice.recordingAudioUnitPropertyLatency * 1000000) + + audioDevice.recordingDelayHWAndOS = audioDevice.recordingDelayHWAndOS.advanced(by: -500) / 1000 + + audioDevice.recordingDelayMeasurementCounter = 0 + } + + audioDevice.recordingDelay = audioDevice.recordingDelayHWAndOS +} diff --git a/Simple-Multiparty/Simple-Multiparty/ViewController.swift b/Simple-Multiparty/Simple-Multiparty/ViewController.swift index b0f8a3a..7014837 100644 --- a/Simple-Multiparty/Simple-Multiparty/ViewController.swift +++ b/Simple-Multiparty/Simple-Multiparty/ViewController.swift @@ -12,11 +12,11 @@ import OpenTok // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key -let kApiKey = "" +let kApiKey = "47521351" // Replace with your generated session ID -let kSessionId = "" +let kSessionId = "2_MX40NzUyMTM1MX5-MTY2MzE4Mjk3NTY1Mn5ZaWtsby9HNVlHSk9Jak40bkZtV3pyZVF-fg" // Replace with your generated token -let kToken = "" +let kToken = "T1==cGFydG5lcl9pZD00NzUyMTM1MSZzaWc9NWQ4Yzc3MjNlNDY5MzYxMjY1NjQ4YjIxZDNmMDUwNjgzYTQwMDRkNTpzZXNzaW9uX2lkPTJfTVg0ME56VXlNVE0xTVg1LU1UWTJNekU0TWprM05UWTFNbjVaYVd0c2J5OUhOVmxIU2s5SmFrNDBia1p0VjNweVpWRi1mZyZjcmVhdGVfdGltZT0xNjYzMTgzMDE4Jm5vbmNlPTAuOTMzMzA0ODAwMjY1NzUzNCZyb2xlPXB1Ymxpc2hlciZleHBpcmVfdGltZT0xNjYzMjY5NDE3JmluaXRpYWxfbGF5b3V0X2NsYXNzX2xpc3Q9" class ViewController: UIViewController { @@ -35,11 +35,14 @@ class ViewController: UIViewController { settings.name = UIDevice.current.name return OTPublisher(delegate: self, settings: settings)! }() + + let customAudioDevice = DefaultAudioDevice() + var error: OTError? override func viewDidLoad() { super.viewDidLoad() - + OTAudioDeviceManager.setAudioDevice(customAudioDevice) session.connect(withToken: kToken, error: &error) userName.text = UIDevice.current.name