diff --git a/example/android/app/src/main/kotlin/io/agora/agora_rtc_engine_example/MainActivity.kt b/example/android/app/src/main/kotlin/io/agora/agora_rtc_engine_example/MainActivity.kt index e82cf4d19..7db1691e3 100644 --- a/example/android/app/src/main/kotlin/io/agora/agora_rtc_engine_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/io/agora/agora_rtc_engine_example/MainActivity.kt @@ -15,11 +15,14 @@ class MainActivity: FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Register the `CustomAudioPlugin` to interect with the `RtcEngine` RtcEnginePlugin.register(customAudioPlugin) } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) + // The `CustomAudioSource` is generated by [pigeon](https://pub.dev/packages/pigeon), you can see the + // the definiton on `example/lib/examples/advanced/custom_audio/custom_audio_source_api.dart` CustomAudioSource.CustomAudioSourceApi.setup(flutterEngine.dartExecutor, customAudioPlugin) } diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 67b4aebff..192e165d6 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,11 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 5E9D826C4B1F29399F8A7742 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28CEF344452C45083E367B0C /* Pods_Runner.framework */; }; 6D48449932DC5CD432350C00 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43CA39DDEDE840FF44D1F750 /* Pods_RunnerTests.framework */; }; + 7110181B2727DA66003816A9 /* CustomAudioSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 7110181A2727DA66003816A9 /* CustomAudioSource.m */; }; + 711018242727DE9A003816A9 /* ExternalAudio.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7110181E2727DE9A003816A9 /* ExternalAudio.mm */; }; + 711018252727DE9A003816A9 /* AudioWriteToFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 711018222727DE9A003816A9 /* AudioWriteToFile.m */; }; + 711018262727DE9A003816A9 /* AudioController.m in Sources */ = {isa = PBXBuildFile; fileRef = 711018232727DE9A003816A9 /* AudioController.m */; }; + 711018282727E05A003816A9 /* CustmoAudioSourcePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711018272727E05A003816A9 /* CustmoAudioSourcePlugin.swift */; }; 71E2A2A22722C75F00C7B7BC /* OCTestRtcEnginePlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E2A29E2722C75F00C7B7BC /* OCTestRtcEnginePlugin.m */; }; 71E2A2A32722C75F00C7B7BC /* FakeAgoraRtcEngineKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2A2A02722C75F00C7B7BC /* FakeAgoraRtcEngineKit.swift */; }; 71E2A2A52722C80800C7B7BC /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2A2A42722C80800C7B7BC /* RunnerTests.swift */; }; @@ -51,6 +56,16 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 43CA39DDEDE840FF44D1F750 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 592BF9E1006B18851CF75B6B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 711018192727DA66003816A9 /* CustomAudioSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomAudioSource.h; sourceTree = ""; }; + 7110181A2727DA66003816A9 /* CustomAudioSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomAudioSource.m; sourceTree = ""; }; + 7110181D2727DE9A003816A9 /* AudioOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioOptions.h; sourceTree = ""; }; + 7110181E2727DE9A003816A9 /* ExternalAudio.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ExternalAudio.mm; sourceTree = ""; }; + 7110181F2727DE9A003816A9 /* ExternalAudio.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExternalAudio.h; sourceTree = ""; }; + 711018202727DE9A003816A9 /* AudioWriteToFile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioWriteToFile.h; sourceTree = ""; }; + 711018212727DE9A003816A9 /* AudioController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioController.h; sourceTree = ""; }; + 711018222727DE9A003816A9 /* AudioWriteToFile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioWriteToFile.m; sourceTree = ""; }; + 711018232727DE9A003816A9 /* AudioController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioController.m; sourceTree = ""; }; + 711018272727E05A003816A9 /* CustmoAudioSourcePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustmoAudioSourcePlugin.swift; sourceTree = ""; }; 71E2A2932722C73000C7B7BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 71E2A29E2722C75F00C7B7BC /* OCTestRtcEnginePlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCTestRtcEnginePlugin.m; sourceTree = ""; }; 71E2A29F2722C75F00C7B7BC /* OCTestRtcEnginePlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCTestRtcEnginePlugin.h; sourceTree = ""; }; @@ -115,6 +130,23 @@ name = Frameworks; sourceTree = ""; }; + 711018292727FFF1003816A9 /* CustomAudioSource */ = { + isa = PBXGroup; + children = ( + 711018212727DE9A003816A9 /* AudioController.h */, + 711018232727DE9A003816A9 /* AudioController.m */, + 7110181D2727DE9A003816A9 /* AudioOptions.h */, + 711018202727DE9A003816A9 /* AudioWriteToFile.h */, + 711018222727DE9A003816A9 /* AudioWriteToFile.m */, + 7110181F2727DE9A003816A9 /* ExternalAudio.h */, + 7110181E2727DE9A003816A9 /* ExternalAudio.mm */, + 711018192727DA66003816A9 /* CustomAudioSource.h */, + 7110181A2727DA66003816A9 /* CustomAudioSource.m */, + 711018272727E05A003816A9 /* CustmoAudioSourcePlugin.swift */, + ); + path = CustomAudioSource; + sourceTree = ""; + }; 71E2A2942722C73000C7B7BC /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -162,6 +194,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 711018292727FFF1003816A9 /* CustomAudioSource */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -394,6 +427,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 711018262727DE9A003816A9 /* AudioController.m in Sources */, + 7110181B2727DA66003816A9 /* CustomAudioSource.m in Sources */, + 711018252727DE9A003816A9 /* AudioWriteToFile.m in Sources */, + 711018242727DE9A003816A9 /* ExternalAudio.mm in Sources */, + 711018282727E05A003816A9 /* CustmoAudioSourcePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); @@ -472,8 +510,9 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -669,7 +708,9 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = NO; }; name = Debug; }; @@ -716,8 +757,9 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4a8..488acd686 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -3,11 +3,31 @@ import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { - override func application( + + private var customAudioSourcePlugin: CustomAudioPlugin! + + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } + ) -> Bool { + let controller : FlutterViewController = window?.rootViewController as! FlutterViewController + + customAudioSourcePlugin = CustomAudioPlugin() + + // The `CustomAudioSourceApiSetup` is generated by [pigeon](https://pub.dev/packages/pigeon), you can see the + // the definiton on `example/lib/examples/advanced/custom_audio/custom_audio_source_api.dart` + CustomAudioSourceApiSetup( + controller.binaryMessenger, customAudioSourcePlugin) + + // Register the `CustomAudioPlugin` to interect with the `AgoraRtcEngineKit` + RtcEnginePluginRegistrant.register(customAudioSourcePlugin) + + GeneratedPluginRegistrant.register(with: self) + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + override func applicationWillTerminate(_ application: UIApplication) { + RtcEnginePluginRegistrant.unregister(customAudioSourcePlugin) + } } diff --git a/example/ios/Runner/CustomAudioSource/AudioController.h b/example/ios/Runner/CustomAudioSource/AudioController.h new file mode 100644 index 000000000..4149e80b9 --- /dev/null +++ b/example/ios/Runner/CustomAudioSource/AudioController.h @@ -0,0 +1,35 @@ +// +// AudioController.h +// AudioCapture +// +// Created by CavanSu on 10/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import +#import +#import "AudioOptions.h" + +@class AudioController; +@protocol AudioControllerDelegate +@optional +- (void)audioController:(AudioController *)controller + didCaptureData:(unsigned char *)data + bytesLength:(int)bytesLength; +- (int)audioController:(AudioController *)controller + didRenderData:(unsigned char *)data + bytesLength:(int)bytesLength; +- (void)audioController:(AudioController *)controller + error:(OSStatus)error + info:(NSString *)info; +@end + + +@interface AudioController : NSObject +@property (nonatomic, weak) id delegate; + ++ (instancetype)audioController; +- (void)setUpAudioSessionWithSampleRate:(int)sampleRate channelCount:(int)channelCount audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType; +- (void)startWork; +- (void)stopWork; + @end diff --git a/example/ios/Runner/CustomAudioSource/AudioController.m b/example/ios/Runner/CustomAudioSource/AudioController.m new file mode 100644 index 000000000..1cd84fa85 --- /dev/null +++ b/example/ios/Runner/CustomAudioSource/AudioController.m @@ -0,0 +1,417 @@ +// +// AudioController.m +// AudioCapture +// +// Created by CavanSu on 10/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import "AudioController.h" +#import "AudioWriteToFile.h" + +#define InputBus 1 +#define OutputBus 0 + +@interface AudioController () +@property (nonatomic, assign) int sampleRate; +@property (nonatomic, assign) int channelCount; +@property (nonatomic, assign) AudioCRMode audioCRMode; +@property (nonatomic, assign) OSStatus error; + +@property (nonatomic, assign) AudioUnit remoteIOUnit; +#if TARGET_OS_MAC +@property (nonatomic, assign) AudioUnit macPlayUnit; +#endif +@end + +@implementation AudioController + +#if TARGET_OS_IPHONE +static double preferredIOBufferDuration = 0.02; +#endif + ++ (instancetype)audioController { + AudioController *audioController = [[self alloc] init]; + return audioController; +} + +#pragma mark - +static OSStatus captureCallBack(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, // inputBus = 1 + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + AudioController *audioController = (__bridge AudioController *)inRefCon; + + AudioUnit captureUnit = [audioController remoteIOUnit]; + + if (!inRefCon) return 0; + + AudioBuffer buffer; + buffer.mData = NULL; + buffer.mDataByteSize = 0; + buffer.mNumberChannels = audioController.channelCount; + + AudioBufferList bufferList; + bufferList.mNumberBuffers = 1; + bufferList.mBuffers[0] = buffer; + + OSStatus status = AudioUnitRender(captureUnit, + ioActionFlags, + inTimeStamp, + inBusNumber, + inNumberFrames, + &bufferList); + + if (!status) { + if ([audioController.delegate respondsToSelector:@selector(audioController:didCaptureData:bytesLength:)]) { + [audioController.delegate audioController:audioController didCaptureData:(unsigned char *)bufferList.mBuffers[0].mData bytesLength:bufferList.mBuffers[0].mDataByteSize]; + } + } + else { + [audioController error:status position:@"captureCallBack"]; + } + + return 0; +} + +#pragma mark - +static OSStatus renderCallBack(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + AudioController *audioController = (__bridge AudioController *)(inRefCon); + + if (*ioActionFlags == kAudioUnitRenderAction_OutputIsSilence) { + return noErr; + } + + int result = 0; + + if ([audioController.delegate respondsToSelector:@selector(audioController:didRenderData:bytesLength:)]) { + result = [audioController.delegate audioController:audioController didRenderData:(uint8_t*)ioData->mBuffers[0].mData bytesLength:ioData->mBuffers[0].mDataByteSize]; + } + + if (result == 0) { + *ioActionFlags = kAudioUnitRenderAction_OutputIsSilence; + ioData->mBuffers[0].mDataByteSize = 0; + } + + return noErr; +} + + +#pragma mark - +- (void)setUpAudioSessionWithSampleRate:(int)sampleRate channelCount:(int)channelCount audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType{ + if (_audioCRMode == AudioCRModeSDKCaptureSDKRender) { + return; + } + + self.audioCRMode = audioCRMode; + self.sampleRate = sampleRate; + self.channelCount = channelCount; + +#if TARGET_OS_IPHONE + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + NSUInteger sessionOption = AVAudioSessionCategoryOptionMixWithOthers; + sessionOption |= AVAudioSessionCategoryOptionAllowBluetooth; + + [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:sessionOption error:nil]; + [audioSession setMode:AVAudioSessionModeDefault error:nil]; + [audioSession setPreferredIOBufferDuration:preferredIOBufferDuration error:nil]; + NSError *error; + BOOL success = [audioSession setActive:YES error:&error]; + if (!success) { + NSLog(@" audioSession setActive:YES error:nil"); + } + if (error) { + NSLog(@" setUpAudioSessionWithSampleRate : %@", error.localizedDescription); + } +#endif + + [self setupRemoteIOWithIOType:ioType]; +} + +#pragma mark - +- (void)setupRemoteIOWithIOType:(IOUnitType)ioType { +#if TARGET_OS_IPHONE + // AudioComponentDescription + AudioComponentDescription remoteIODesc; + remoteIODesc.componentType = kAudioUnitType_Output; + remoteIODesc.componentSubType = ioType == IOUnitTypeVPIO ? kAudioUnitSubType_VoiceProcessingIO : kAudioUnitSubType_RemoteIO; + remoteIODesc.componentManufacturer = kAudioUnitManufacturer_Apple; + remoteIODesc.componentFlags = 0; + remoteIODesc.componentFlagsMask = 0; + AudioComponent remoteIOComponent = AudioComponentFindNext(NULL, &remoteIODesc); + _error = AudioComponentInstanceNew(remoteIOComponent, &_remoteIOUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; +#endif + + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + +#if !TARGET_OS_IPHONE + AudioComponentDescription remoteIODesc; + remoteIODesc.componentType = kAudioUnitType_Output; + remoteIODesc.componentSubType = kAudioUnitSubType_HALOutput; + remoteIODesc.componentManufacturer = kAudioUnitManufacturer_Apple; + remoteIODesc.componentFlags = 0; + remoteIODesc.componentFlagsMask = 0; + AudioComponent remoteIOComponent = AudioComponentFindNext(NULL, &remoteIODesc); + _error = AudioComponentInstanceNew(remoteIOComponent, &_remoteIOUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; + _error = AudioUnitInitialize(_remoteIOUnit); + [self error:_error position:@"AudioUnitInitialize"]; +#endif + [self setupCapture]; + } + + if (_audioCRMode == AudioCRModeSDKCaptureExterRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + +#if !TARGET_OS_IPHONE + AudioComponentDescription macPlayDesc; + macPlayDesc.componentType = kAudioUnitType_Output; + macPlayDesc.componentSubType = kAudioUnitSubType_DefaultOutput; + macPlayDesc.componentManufacturer = kAudioUnitManufacturer_Apple; + macPlayDesc.componentFlags = 0; + macPlayDesc.componentFlagsMask = 0; + AudioComponent macPlayComponent = AudioComponentFindNext(NULL, &macPlayDesc); + _error = AudioComponentInstanceNew(macPlayComponent, &_macPlayUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; + _error = AudioUnitInitialize(_macPlayUnit); + [self error:_error position:@"AudioUnitInitialize"]; +#endif + [self setupRender]; + } + +} + +- (void)setupCapture { + // EnableIO + UInt32 one = 1; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Input, + InputBus, + &one, + sizeof(one)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input"]; + +#if !TARGET_OS_IPHONE + UInt32 disableFlag = 0; + + // Attention! set kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, disable + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + OutputBus, + &disableFlag, + sizeof(disableFlag)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output"]; + + AudioDeviceID defaultDevice = kAudioDeviceUnknown; + UInt32 propertySize = sizeof(defaultDevice); + AudioObjectPropertyAddress defaultDeviceProperty = { + .mSelector = kAudioHardwarePropertyDefaultInputDevice, + .mScope = kAudioObjectPropertyScopeInput, + .mElement = kAudioObjectPropertyElementMaster + }; + + _error = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &defaultDeviceProperty, + 0, + NULL, + &propertySize, + &defaultDevice); + [self error:_error position:@"AudioObjectGetPropertyData, kAudioObjectSystemObject"]; + + // Set the sample rate of the input device to the output samplerate (if possible) + Float64 temp = _sampleRate; + defaultDeviceProperty.mSelector = kAudioDevicePropertyNominalSampleRate; + + _error = AudioObjectSetPropertyData(defaultDevice, + &defaultDeviceProperty, + 0, + NULL, + sizeof(Float64), + &temp); + [self error:_error position:@"AudioObjectSetPropertyData, defaultDeviceProperty"]; + + // Set the input device to the system's default input device + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + InputBus, + &defaultDevice, + sizeof(defaultDevice)); + [self error:_error position:@"kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global"]; + +#endif + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + InputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output"]; + + // CallBack + AURenderCallbackStruct captureCallBackStruck; + captureCallBackStruck.inputProcRefCon = (__bridge void * _Nullable)(self); + captureCallBackStruck.inputProc = captureCallBack; + + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + InputBus, + &captureCallBackStruck, + sizeof(captureCallBackStruck)); + [self error:_error position:@"kAudioOutputUnitProperty_SetInputCallback"]; +} + +- (void)setupRender { + +#if TARGET_OS_IPHONE + // EnableIO + UInt32 one = 1; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + OutputBus, + &one, + sizeof(one)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output"]; + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + OutputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input"]; + + // CallBack + AURenderCallbackStruct renderCallback; + renderCallback.inputProcRefCon = (__bridge void * _Nullable)(self); + renderCallback.inputProc = renderCallBack; + AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + OutputBus, + &renderCallback, + sizeof(renderCallback)); + [self error:_error position:@"kAudioUnitProperty_SetRenderCallback"]; + +#else + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_macPlayUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + OutputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input"]; + + // CallBack + AURenderCallbackStruct renderCallback; + renderCallback.inputProcRefCon = (__bridge void * _Nullable)(self); + renderCallback.inputProc = renderCallBack; + _error = AudioUnitSetProperty(_macPlayUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + OutputBus, + &renderCallback, + sizeof(renderCallback)); + [self error:_error position:@"kAudioUnitProperty_SetRenderCallback"]; +#endif + +} + +- (void)startWork { +#if TARGET_OS_IPHONE + _error = AudioOutputUnitStart(_remoteIOUnit); + [self error:_error position:@"AudioOutputUnitStart"]; +#else + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + _error = AudioOutputUnitStart(_remoteIOUnit); + if (_error != noErr) { + [self error:_error position:@"AudioOutputUnitStart"]; + return; + } + } + + if (self.audioCRMode == AudioCRModeExterCaptureExterRender || self.audioCRMode == AudioCRModeSDKCaptureExterRender) { + _error = AudioOutputUnitStart(_macPlayUnit); + [self error:_error position:@"AudioOutputUnitStart"]; + } +#endif +} + +- (void)stopWork { +#if TARGET_OS_IPHONE + AudioOutputUnitStop(_remoteIOUnit); +#else + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + AudioOutputUnitStop(_remoteIOUnit); + } + + if (self.audioCRMode == AudioCRModeExterCaptureExterRender || self.audioCRMode == AudioCRModeSDKCaptureExterRender) { + AudioOutputUnitStop(_macPlayUnit); + } +#endif +} + +- (void)error:(OSStatus)error position:(NSString *)position { + if (error != noErr) { + NSString *errorInfo = [NSString stringWithFormat:@" Error: %d, Position: %@", (int)error, position]; + if ([self.delegate respondsToSelector:@selector(audioController:error:info:)]) { + [self.delegate audioController:self error:error info:position]; + } + NSLog(@" :%@", errorInfo); + } +} + +- (AudioStreamBasicDescription)signedIntegerStreamFormatDesc { + AudioStreamBasicDescription streamFormatDesc; + streamFormatDesc.mSampleRate = _sampleRate; + streamFormatDesc.mFormatID = kAudioFormatLinearPCM; + streamFormatDesc.mFormatFlags = (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked); + streamFormatDesc.mChannelsPerFrame = _channelCount; + streamFormatDesc.mFramesPerPacket = 1; + streamFormatDesc.mBitsPerChannel = 16; + streamFormatDesc.mBytesPerFrame = streamFormatDesc.mBitsPerChannel / 8 * streamFormatDesc.mChannelsPerFrame; + streamFormatDesc.mBytesPerPacket = streamFormatDesc.mBytesPerFrame * streamFormatDesc.mFramesPerPacket; + + return streamFormatDesc; +} + +- (void)dealloc { + if (_remoteIOUnit) { + AudioOutputUnitStop(_remoteIOUnit); + AudioComponentInstanceDispose(_remoteIOUnit); + _remoteIOUnit = nil; + } + +#if !TARGET_OS_IPHONE + if (_macPlayUnit) { + AudioOutputUnitStop(_macPlayUnit); + AudioComponentInstanceDispose(_macPlayUnit); + _macPlayUnit = nil; + } +#endif + + NSLog(@" AudioController dealloc"); +} + +@end diff --git a/example/ios/Runner/CustomAudioSource/AudioOptions.h b/example/ios/Runner/CustomAudioSource/AudioOptions.h new file mode 100644 index 000000000..9c6e8f43b --- /dev/null +++ b/example/ios/Runner/CustomAudioSource/AudioOptions.h @@ -0,0 +1,40 @@ +// +// AudioOptions.h +// AgoraAudioIO +// +// Created by CavanSu on 12/03/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#ifndef AudioOptions_h +#define AudioOptions_h + +typedef NS_ENUM(int, AudioCRMode) { + AudioCRModeExterCaptureSDKRender = 1, + AudioCRModeSDKCaptureExterRender = 2, + AudioCRModeSDKCaptureSDKRender = 3, + AudioCRModeExterCaptureExterRender = 4 +}; + +typedef NS_ENUM(int, IOUnitType) { + IOUnitTypeVPIO, + IOUnitTypeRemoteIO +}; + +typedef NS_ENUM(int, ChannelMode) { + ChannelModeCommunication = 0, + ChannelModeLiveBroadcast = 1 +}; + +typedef NS_ENUM(int, ClientRole) { + ClientRoleAudience = 0, + ClientRoleBroadcast = 1 +}; + +//#if TARGET_OS_IPHONE +//#import "UIColor+CSRGB.h" +//#import "UIView+CSshortFrame.h" +//#define ThemeColor [UIColor Red: 122 Green: 203 Blue: 253] +//#endif + +#endif /* AudioOptions_h */ diff --git a/example/ios/Runner/CustomAudioSource/AudioWriteToFile.h b/example/ios/Runner/CustomAudioSource/AudioWriteToFile.h new file mode 100644 index 000000000..9ccf24b14 --- /dev/null +++ b/example/ios/Runner/CustomAudioSource/AudioWriteToFile.h @@ -0,0 +1,13 @@ +// +// AudioWriteToFile.h +// AudioCapture +// +// Created by CavanSu on 08/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import + +@interface AudioWriteToFile : NSObject ++ (void)writeToFileWithData:(void *)data length:(int)bytes; +@end diff --git a/example/ios/Runner/CustomAudioSource/AudioWriteToFile.m b/example/ios/Runner/CustomAudioSource/AudioWriteToFile.m new file mode 100644 index 000000000..54558635a --- /dev/null +++ b/example/ios/Runner/CustomAudioSource/AudioWriteToFile.m @@ -0,0 +1,39 @@ +// +// AudioWriteToFile.m +// AudioCapture +// +// Created by CavanSu on 08/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import "AudioWriteToFile.h" + +@implementation AudioWriteToFile + +static NSFileHandle *file = nil; +static dispatch_queue_t queue = nil; + ++ (void)load { + queue = dispatch_queue_create("writeFile", NULL); +} + ++ (void)writeToFileWithData:(void *)data length:(int)bytes { + if(NULL == data || bytes < 1) return; + + dispatch_async(queue, ^{ + + if (file == nil) { + NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"1.pcm"]; + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + if (![[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil]) { + + } + else { + file = [NSFileHandle fileHandleForWritingAtPath:path]; + } + } + [file writeData:[NSData dataWithBytes:data length:bytes]]; + }); +} + +@end diff --git a/example/ios/Runner/CustomAudioSource/CustmoAudioSourcePlugin.swift b/example/ios/Runner/CustomAudioSource/CustmoAudioSourcePlugin.swift new file mode 100644 index 000000000..092eeffc0 --- /dev/null +++ b/example/ios/Runner/CustomAudioSource/CustmoAudioSourcePlugin.swift @@ -0,0 +1,50 @@ +import Foundation +import agora_rtc_engine + +class CustomAudioPlugin : NSObject, RtcEnginePlugin, CustomAudioSourceApi { + + private var agoraRtcEngineKit: AgoraRtcEngineKit? = nil + + private var exAudio: ExternalAudio? = nil + + func onRtcEngineCreated(_ rtcEngine: AgoraRtcEngineKit?) { + agoraRtcEngineKit = rtcEngine + } + + func onRtcEngineDestroyed() { + agoraRtcEngineKit = nil + } + + func setExternalAudioSourceEnabled(_ enabled: NSNumber, sampleRate: NSNumber, channels: NSNumber, error: AutoreleasingUnsafeMutablePointer) { + agoraRtcEngineKit?.enableExternalAudioSource( + withSampleRate: UInt(truncating: sampleRate), + channelsPerFrame: UInt(truncating: channels)) + } + + func setExternalAudioSourceVolumeSourcePos(_ sourcePos: NSNumber, volume: NSNumber, error: AutoreleasingUnsafeMutablePointer) { + let pos = AgoraAudioExternalSourcePos( + rawValue: UInt(truncating: sourcePos))! + agoraRtcEngineKit?.setExternalAudioSourceVolume( + pos, + volume: UInt(truncating: volume)) + } + + func startAudioRecordSampleRate(_ sampleRate: NSNumber, channels: NSNumber, error: AutoreleasingUnsafeMutablePointer) { + exAudio = ExternalAudio.shared() + + exAudio?.setupExternalAudio( + withAgoraKit: agoraRtcEngineKit!, + sampleRate: UInt32(truncating: sampleRate), + channels: UInt32(truncating: channels), + audioCRMode: .exterCaptureSDKRender, + ioType: .remoteIO) + + exAudio?.startWork() + } + + func stopAudioRecordWithError(_ error: AutoreleasingUnsafeMutablePointer) { + exAudio?.stopWork() + exAudio = nil + agoraRtcEngineKit?.disableExternalAudioSource() + } +} diff --git a/example/ios/Runner/CustomAudioSource.h b/example/ios/Runner/CustomAudioSource/CustomAudioSource.h similarity index 100% rename from example/ios/Runner/CustomAudioSource.h rename to example/ios/Runner/CustomAudioSource/CustomAudioSource.h diff --git a/example/ios/Runner/CustomAudioSource.m b/example/ios/Runner/CustomAudioSource/CustomAudioSource.m similarity index 100% rename from example/ios/Runner/CustomAudioSource.m rename to example/ios/Runner/CustomAudioSource/CustomAudioSource.m diff --git a/example/ios/Runner/CustomAudioSource/ExternalAudio.h b/example/ios/Runner/CustomAudioSource/ExternalAudio.h new file mode 100644 index 000000000..17e1cb3a1 --- /dev/null +++ b/example/ios/Runner/CustomAudioSource/ExternalAudio.h @@ -0,0 +1,26 @@ +// +// ExternalAudio.h +// AgoraAudioIO +// +// Created by CavanSu on 22/01/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#import +#import "AudioOptions.h" + +@class AgoraRtcEngineKit; +@class ExternalAudio; +@protocol ExternalAudioDelegate +@optional +- (void)externalAudio:(ExternalAudio *)externalAudio errorInfo:(NSString *)errorInfo; +@end + +@interface ExternalAudio : NSObject +@property (nonatomic, weak) id delegate; + ++ (instancetype)sharedExternalAudio; +- (void)setupExternalAudioWithAgoraKit:(AgoraRtcEngineKit *)agoraKit sampleRate:(uint)sampleRate channels:(uint)channels audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType; +- (void)startWork; +- (void)stopWork; +@end diff --git a/example/ios/Runner/CustomAudioSource/ExternalAudio.mm b/example/ios/Runner/CustomAudioSource/ExternalAudio.mm new file mode 100644 index 000000000..04bae4402 --- /dev/null +++ b/example/ios/Runner/CustomAudioSource/ExternalAudio.mm @@ -0,0 +1,310 @@ +// +// ExternalAudio.m +// AgoraAudioIO +// +// Created by CavanSu on 22/01/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#import "ExternalAudio.h" +#import "AudioController.h" +#import "AudioWriteToFile.h" + +#if TARGET_OS_IPHONE +#import +#import +#import +#else +#import +#import +#import +#endif + +@interface ExternalAudio () +@property (nonatomic, strong) AudioController *audioController; +@property (nonatomic, assign) AudioCRMode audioCRMode; +@property (nonatomic, assign) int sampleRate; +@property (nonatomic, assign) int channelCount; +@property (nonatomic, weak) AgoraRtcEngineKit *agoraKit; +@end + +@implementation ExternalAudio + +static NSObject *threadLockCapture; +static NSObject *threadLockPlay; + +#pragma mark - C++ ExternalAudioFrameObserver +class ExternalAudioFrameObserver : public agora::media::IAudioFrameObserver +{ +private: + + // total buffer length of per second + enum { kBufferLengthBytes = 441 * 2 * 2 * 50 }; // + + // capture + char byteBuffer[kBufferLengthBytes]; // char take up 1 byte, byterBuffer[] take up 88200 bytes + int readIndex = 0; + int writeIndex = 0; + int availableBytes = 0; + int channels = 1; + + // play + char byteBuffer_play[kBufferLengthBytes]; + int readIndex_play = 0; + int writeIndex_play = 0; + int availableBytes_play = 0; + int channels_play = 1; + +public: + int sampleRate = 0; + int sampleRate_play = 0; + + bool isExternalCapture = false; + bool isExternalRender = false; + +#pragma mark- + // push audio data to special buffer(Array byteBuffer) + // bytesLength = date length + void pushExternalData(void* data, int bytesLength) + { + @synchronized(threadLockCapture) { + + if (availableBytes + bytesLength > kBufferLengthBytes) { + + readIndex = 0; + writeIndex = 0; + availableBytes = 0; + } + + if (writeIndex + bytesLength > kBufferLengthBytes) { + + int left = kBufferLengthBytes - writeIndex; + memcpy(byteBuffer + writeIndex, data, left); + memcpy(byteBuffer, (char *)data + left, bytesLength - left); + writeIndex = bytesLength - left; + } + else { + + memcpy(byteBuffer + writeIndex, data, bytesLength); + writeIndex += bytesLength; + } + availableBytes += bytesLength; + } + + } + + // copy byteBuffer to audioFrame.buffer + virtual bool onRecordAudioFrame(AudioFrame& audioFrame) override + { + @synchronized(threadLockCapture) { + + if (isExternalCapture == false) return true; + + int readBytes = sampleRate / 100 * channels * audioFrame.bytesPerSample; + + if (availableBytes < readBytes) { + return false; + } + + audioFrame.samplesPerSec = sampleRate; + unsigned char tmp[960]; // The most rate:@48k fs, channels = 1, the most total size = 960; + + if (readIndex + readBytes > kBufferLengthBytes) { + int left = kBufferLengthBytes - readIndex; + memcpy(tmp, byteBuffer + readIndex, left); + memcpy(tmp + left, byteBuffer, readBytes - left); + readIndex = readBytes - left; + } + else { + memcpy(tmp, byteBuffer + readIndex, readBytes); + readIndex += readBytes; + } + + availableBytes -= readBytes; + + if (channels == audioFrame.channels) { + memcpy(audioFrame.buffer, tmp, readBytes); + } + [AudioWriteToFile writeToFileWithData:audioFrame.buffer length:readBytes]; + return true; + } + + } + +#pragma mark- + // read Audio data from byteBuffer_play to audioUnit + int readAudioData(void* data, int bytesLength) + { + @synchronized(threadLockPlay) { + + if (NULL == data || bytesLength < 1 || availableBytes_play < bytesLength) { + return 0; + } + + int readBytes = bytesLength; + + unsigned char tmp[4096]; // unsigned char takes up 1 byte + + if (readIndex_play + readBytes > kBufferLengthBytes) { + + int left = kBufferLengthBytes - readIndex_play; + memcpy(tmp, byteBuffer_play + readIndex_play, left); + memcpy(tmp + left, byteBuffer_play, readBytes - left); + readIndex_play = readBytes - left; + } + else { + + memcpy(tmp, byteBuffer_play + readIndex_play, readBytes); + readIndex_play += readBytes; + } + + availableBytes_play -= readBytes; + + if (channels_play == 1) { + memcpy(data, tmp, readBytes); + } + + [AudioWriteToFile writeToFileWithData:data length:readBytes]; + + return readBytes; + } + + } + + // recive remote audio stream, push audio data to byteBuffer_play + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame) override + { + @synchronized(threadLockPlay) { + + if (isExternalRender == false) return true; + + int bytesLength = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + char *data = (char *)audioFrame.buffer; + + sampleRate_play = audioFrame.samplesPerSec; + channels_play = audioFrame.channels; + + if (availableBytes_play + bytesLength > kBufferLengthBytes) { + + readIndex_play = 0; + writeIndex_play = 0; + availableBytes_play = 0; + } + + if (writeIndex_play + bytesLength > kBufferLengthBytes) { + + int left = kBufferLengthBytes - writeIndex_play; + memcpy(byteBuffer_play + writeIndex_play, data, left); + memcpy(byteBuffer_play, (char *)data + left, bytesLength - left); + writeIndex_play = bytesLength - left; + } + else { + + memcpy(byteBuffer_play + writeIndex_play, data, bytesLength); + writeIndex_play += bytesLength; + } + + availableBytes_play += bytesLength; + + return true; + } + + } + + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame) override { return true; } + + virtual bool onMixedAudioFrame(AudioFrame& audioFrame) override { return true; } +}; + +static ExternalAudioFrameObserver* s_audioFrameObserver; + + ++ (instancetype)sharedExternalAudio { + ExternalAudio *audio = [[ExternalAudio alloc] init]; + return audio; +} + +- (void)setupExternalAudioWithAgoraKit:(AgoraRtcEngineKit *)agoraKit sampleRate:(uint)sampleRate channels:(uint)channels audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType { + + threadLockCapture = [[NSObject alloc] init]; + threadLockPlay = [[NSObject alloc] init]; + + // AudioController + self.audioController = [AudioController audioController]; + self.audioController.delegate = self; + [self.audioController setUpAudioSessionWithSampleRate:sampleRate channelCount:channels audioCRMode:audioCRMode IOType:ioType]; + + // Agora Engine of C++ + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + if (mediaEngine) { + s_audioFrameObserver = new ExternalAudioFrameObserver(); + s_audioFrameObserver -> sampleRate = sampleRate; + s_audioFrameObserver -> sampleRate_play = channels; + mediaEngine->registerAudioFrameObserver(s_audioFrameObserver); + } + + if (audioCRMode == AudioCRModeExterCaptureExterRender || audioCRMode == AudioCRModeSDKCaptureExterRender) { + s_audioFrameObserver -> isExternalRender = true; + } + if (audioCRMode == AudioCRModeExterCaptureExterRender || audioCRMode == AudioCRModeExterCaptureSDKRender) { + s_audioFrameObserver -> isExternalCapture = true; + } + + self.agoraKit = agoraKit; + self.audioCRMode = audioCRMode; +} + +- (void)startWork { + [self.audioController startWork]; +} + +- (void)stopWork { + [self.audioController stopWork]; + [self cancelRegiset]; +} + +- (void)cancelRegiset { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + mediaEngine->registerAudioFrameObserver(NULL); +} + +- (void)audioController:(AudioController *)controller didCaptureData:(unsigned char *)data bytesLength:(int)bytesLength { + + if (self.audioCRMode != AudioCRModeExterCaptureSDKRender) { + if (s_audioFrameObserver) { + s_audioFrameObserver -> pushExternalData(data, bytesLength); + } + } + else { + [self.agoraKit pushExternalAudioFrameRawData:data samples:bytesLength / 2 timestamp:0]; + } + +} + +- (int)audioController:(AudioController *)controller didRenderData:(unsigned char *)data bytesLength:(int)bytesLength { + int result = 0; + + if (s_audioFrameObserver) { + result = s_audioFrameObserver -> readAudioData(data, bytesLength); + } + + return result; +} + +- (void)audioController:(AudioController *)controller error:(OSStatus)error info:(NSString *)info { + if ([self.delegate respondsToSelector:@selector(externalAudio:errorInfo:)]) { + NSString *errorInfo = [NSString stringWithFormat:@" error:%d, info:%@", error, info]; + [self.delegate externalAudio:self errorInfo:errorInfo]; + } +} + +- (void)dealloc { + NSLog(@"ExAudio dealloc"); +} + +@end diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h index 308a2a560..4127e4215 100644 --- a/example/ios/Runner/Runner-Bridging-Header.h +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -1 +1,13 @@ #import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#endif +#if __has_include() +#import +#endif + +#import "CustomAudioSource.h" +#import "ExternalAudio.h" + + diff --git a/example/lib/examples/advanced/custom_audio/custom_audio_source.dart b/example/lib/examples/advanced/custom_audio/custom_audio_source.dart index ea1e36f79..0c76cb476 100644 --- a/example/lib/examples/advanced/custom_audio/custom_audio_source.dart +++ b/example/lib/examples/advanced/custom_audio/custom_audio_source.dart @@ -116,10 +116,10 @@ class _CustomAudioSourceState extends State { await _engine.joinChannel(config.token, config.channelId, null, 0, option); } - void _destroyEngine() { - _api.stopAudioRecord(); - _engine.leaveChannel(); - _engine.destroy(); + void _destroyEngine() async { + await _api.stopAudioRecord(); + await _engine.leaveChannel(); + await _engine.destroy(); } void _sourcePosChanged() {