회사에서 더욱 빠른 영상을 전달하기 위해서 webRTC를 도입하게 되었습니다.
webRTC를 도입하는 과정에서 개인적으로 자료도 없고 귀찮았던 부분들을 앞으로 몇가지 기술할 예정입니다.
그 첫번째 주자로써는 바로 mic 권한 빼기인데요.
webRTC 기술이 탄생한 배경 자체가 양방향 통신이라 googlWebRTC 프레임워크를 받고서 시작하면 자동으로 마이크 권한을 요구하는 것을 볼 수 있습니다.
하지만 저희 회사 같이 시청 전용으로 기술이 제공될때는 필수적인 요소가 아니게되는데요. 지금부터 어떻게 하면 마이크 권한을 뺄 수 있는지에 적어보겠습니다.
먼저 간단하게나마 AudioUnit에 대해서 짚고 넘어가야 합니다.
AVAudioUnit
AVAudioUnit은 iOS에서 사용할 수 있는 가장 low-level한 Audio 객체 중 하나입니다.
역시 성능 가장 우수하여 audio latency또한 가장 빨라서 실시간성에 가장 강한 면모를 보입니다.
특징으로는 다음 2개의 요소를 알고 넘어가면 되겠습니다.
1. Scope의 개념
AVAudio에는 3개의 Scope이 존재하는 데 다음과 같습니다.
Input, Output, global
위의 3가지 scope AVAudioUnit마다 별개로 존재하며 각 Scope와 대응되는 Element를 가지고 있습니다.
2.Element의 개념
Element의 경우 Input과 Output의 개념으로 나누어집니다.
그리고 AVAudioUnit이 어떤 타입인지에 대해서 개수가 나누어지는데 예를 들면 일반적인 I/O 같은 타입의 AVAudioUnit은
Input 1개 Output1개로 나누어지는데 반해, mixer 타입의 경우 Input n개 Output 1개를 가지게 됩니다.
3. Scope/Element/Unit의 관계도
이번 RTC에서 마이크 권한 없애기의 가장 중요한 개념인데요. 바로 AVAudioUnit이 어떻게 작동하는지 입니다.
특히 I/O 타입에 대해서 알아볼건데요. 그림을 보면 다음과 같이 구성됩니다.
Scope는 명화하져, 그러면 Element 0 과 1은 뭘까요?
문서의 설명을 보면 다음과 같이 되어 있습니다.
- The input element is element 1 (mnemonic device: the letter “I” of the word “Input” has an appearance similar to the number 1)
- The output element is element 0 (mnemonic device: the letter “O” of the word “Output” has an appearance similar to the number 0)
쉽세 말해 Element 1은 인풋, Element 0 은 아웃풋.
그러면 여기서 우리는 무엇을 해야 할까요? 또 RTC는 어떻게 AVAudioUnit을 설정하고 있을까요?
RTC framework 까보기
Rtc 프레임워크를 직접 다운받고 빌드하고 뜯고 맛보고 즐기다 보시면
all>sdk>objc>native>src>audio>voice_processing_audio_unit.m 파일을 보실 수 있을 겁니다.
그 중에서도 우리는 다음 함수를 수정하겠습니다.
bool VoiceProcessingAudioUnit::Init() {
RTC_DCHECK_EQ(state_, kInitRequired);
// Create an audio component description to identify the Voice Processing
// I/O audio unit.
AudioComponentDescription vpio_unit_description;
vpio_unit_description.componentType = kAudioUnitType_Output;
vpio_unit_description.componentSubType = kAudioUnitSubType_VoiceProcessingIO;
vpio_unit_description.componentManufacturer = kAudioUnitManufacturer_Apple;
vpio_unit_description.componentFlags = 0;
vpio_unit_description.componentFlagsMask = 0;
// Obtain an audio unit instance given the description.
AudioComponent found_vpio_unit_ref =
AudioComponentFindNext(nullptr, &vpio_unit_description);
// Create a Voice Processing IO audio unit.
OSStatus result = noErr;
result = AudioComponentInstanceNew(found_vpio_unit_ref, &vpio_unit_);
if (result != noErr) {
vpio_unit_ = nullptr;
RTCLogError(@"AudioComponentInstanceNew failed. Error=%ld.", (long)result);
return false;
}
//MARK: - 여기
RTCAudioSessionConfiguration *webRTCConfiguration = [RTCAudioSessionConfiguration webRTCConfiguration];
//MARK: - 여기
if (webRTCConfiguration.isMicrophoneMute == false) {
// Enable input on the input scope of the input element.
UInt32 enable_input = 1;
result = AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, kInputBus, &enable_input,
sizeof(enable_input));
if (result != noErr) {
DisposeAudioUnit();
RTCLogError(@"Failed to enable input on input scope of input element. "
"Error=%ld.",
(long)result);
return false;
}
}
else {
}
// Enable output on the output scope of the output element.
UInt32 enable_output = 1;
result = AudioUnitSetProperty(vpio_unit_, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, kOutputBus,
&enable_output, sizeof(enable_output));
if (result != noErr) {
DisposeAudioUnit();
RTCLogError(@"Failed to enable output on output scope of output element. "
"Error=%ld.",
(long)result);
return false;
}
// Specify the callback function that provides audio samples to the audio
// unit.
AURenderCallbackStruct render_callback;
render_callback.inputProc = OnGetPlayoutData;
render_callback.inputProcRefCon = this;
result = AudioUnitSetProperty(
vpio_unit_, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input,
kOutputBus, &render_callback, sizeof(render_callback));
if (result != noErr) {
DisposeAudioUnit();
RTCLogError(@"Failed to specify the render callback on the output bus. "
"Error=%ld.",
(long)result);
return false;
}
// Disable AU buffer allocation for the recorder, we allocate our own.
// TODO(henrika): not sure that it actually saves resource to make this call.
//MARK: - 여기
if (webRTCConfiguration.isMicrophoneMute == false ) {
UInt32 flag = 0;
result = AudioUnitSetProperty(
vpio_unit_, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, kInputBus, &flag, sizeof(flag));
if (result != noErr) {
DisposeAudioUnit();
RTCLogError(@"Failed to disable buffer allocation on the input bus. "
"Error=%ld.",
(long)result);
return false;
}
}
// Specify the callback to be called by the I/O thread to us when input audio
// is available. The recorded samples can then be obtained by calling the
// AudioUnitRender() method.
//MARK: - 여기
if (webRTCConfiguration.isMicrophoneMute == false) {
AURenderCallbackStruct input_callback;
input_callback.inputProc = OnDeliverRecordedData;
input_callback.inputProcRefCon = this;
result = AudioUnitSetProperty(vpio_unit_,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, kInputBus,
&input_callback, sizeof(input_callback));
if (result != noErr) {
DisposeAudioUnit();
RTCLogError(@"Failed to specify the input callback on the input bus. "
"Error=%ld.",
(long)result);
return false;
}
}
state_ = kUninitialized;
return true;
}
수정해야 하는 부준 Mark를 달아 두었습니다.
간단합니다.
위에서 보셨던 AVAudioUnit의 관계를 이해하셨다면, 코드를 읽어 보셨을때 이해하시리라 믿습니다.
제가 한 것이라고는 제가 정의한 isMicrophoneMute라는 변수를 읽어와서 만약에서 true라면
Element 1의 인풋을 없애 버리는 작업을 한게 다입니다.
자세하게는 보시려면 AudioUnitSetProperty 인자에서 Input인지 Ouput인지를 보면 됩니다.
추가적으로 마지막 즈음 보시면 callback도 정의하지 않은 것을 보실 수 있습니다.
정말 이게 다구요.
정확한 코드는 나중에 깃헙의 링크를 달겠습니다.
'iOS' 카테고리의 다른 글
CFSocket을 통한 로컬 서버 만들기 5 (0) | 2024.05.02 |
---|---|
[Swift] AVAssetDownLoadTask 사용하기 (1) | 2023.12.03 |
CFSocket을 통한 로컬 서버 만들기 4 (1) | 2023.10.22 |
CFSocket을 통한 로컬 서버 만들기 3 (0) | 2023.06.26 |
CFSocket을 통한 로컬 서버 만들기 2 (0) | 2023.06.26 |