[002] 플러터 (Flutter) & 아고라 (Agora) 연동 - Flutter 음성 통화(Voice Call) UI 및 기능 구현

2024. 4. 2. 22:37모바일어플개발/Flutter & Agora 연동

반응형

안녕하세요~ totally 개발자입니다.

 

지난 포스팅에서는 아고라(Agora) 기본 세팅을 마무리하였고 이번 포스팅에서는 Agora를 이용해서 플러터에서 음성 통화를 구현해보도록 하겠습니다. 안드로이드 및 아이폰 실기기로 직접 작업해서 테스트하였습니다. 코드는 agora의 공식 문서를 따라 작성하였습니다.

https://docs.agora.io/en/voice-calling/get-started/get-started-sdk?platform=flutter

 

https://docs.agora.io/en/voice-calling/get-started/get-started-sdk?platform=flutter

 

docs.agora.io

 

Step 1: 플러터 프로젝트를 새로 만들어줍니다. 저는 agora_project라고 명명하였습니다.

 

Step 2: pubspec.yaml 파일을 열어서 agora_rtc_engine 패키지와 permission_handler 패키지를 추가해줍니다.

 

Step 3: Android 설정을 위해 android > app > src > main > AndroidManifest.xml 파일을 열어서 아래 7줄을 추가합니다. 추후 영상통화도 구현하기 때문에 카메라 권한도 추가해줍니다.

<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

 

 

 

Step 4: IOS 세팅을 위해 ios > Runner > info.plist에 다음 내용을 추가합니다.

<key>NSMicrophoneUsageDescription</key>
<string>Talk To Others</string>
<key>NSCameraUsageDescription</key>
<string>Video Call With Others</string>

 

 

Step 5: main.dart 파일을 열어주시고 기본 세팅을 해줍니다. 저는 MainApp이라는 StatefulWidget 상속 받는 클래스를 만들었습니다. setState 상태 업데이트가 필요하여 StatelessWidget이 아닌 StatefulWidget을 사용합니다.

 

Step 6: Agora 콘솔 페이지에 들어가서 App ID를 복사해서 플러터 appId 변수에 넣어줍니다.

 

Step 7: Agora 콘솔 페이지에 들어가셔서 Generate temp RTC Token을 눌러줍니다.

그리고 Channel Name을 입력하고 Generate하시면 아래 화면처럼 Temp Token이 발급됩니다. 복사해주세요.

 

 

Step 8: 그 후 변수 channelName과 token를 선언해주시고 복사한 값을 넣어주시면 됩니다.

 

 

Step 9: token 밑에 uid, _remoteUid, _isJoined, agoraEngine, scaffoldMessengerKey 변수들을 작성합니다. 전체 코드는 맨 아래에 있습니다. 그리고 showMessage 메소드를 만들어서 snackBar 형태로 문구를 출력해주도록 합니다.

int uid = 0;

int? _remoteUid;
bool _isJoined = false;
late RtcEngine agoraEngine;

final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();

showMessage(String message) {
scaffoldMessengerKey.currentState
?.showSnackBar(SnackBar(content: Text(message)));
}

 

uid는 user ID로 설명은 아래와 같이 나와 있습니다. 0으로 세팅되어 있는 경우 SDK가 임의로 user ID를 부여합니다. 이 uid는 채널 안에 있는 사용자를 구별하기 위해 사용됩니다. 즉 uid는 본인이 됩니다.

  • The user ID. This parameter is used to identify the user in the channel for real-time audio and video interaction. You need to set and manage user IDs yourself, and ensure that each user ID in the same channel is unique. This parameter is a 32-bit unsigned integer. The value range is 1 to 2 32 -1. If the user ID is not assigned (or set to 0), the SDK assigns a random user ID and returns it in the onJoinChannelSuccess callback. Your app must record and maintain the returned user ID, because the SDK does not do so.

 

uid가 로컬 사용자(본인)이므로 remoteUid는 상대방이 됩니다. 

 

Step 10: 아래 부분을 작성합니다. initState를 통해 SDK 엔진을 먼저 생성해주시고 dispose를 통해 종료되는 경우 채널을 나가도록 설정합니다. setupVoiceSDKEngine 메소드를 보면, 마이크 권한을 요청하거나 가져옵니다. 그런 다음, Agora RTC Engine을 만들어주고 appId를 할당하여 초기화 작업을 진행합니다. agoraEngine.registerEventHandler를 통해 onJoinChannelSuccess(로컬 사용자가 채널에 조인하는 경우), onUserJoined(상대방이 채널에 조인하는 경우), onUserOffline(상대방이 채널을 떠나서 오프라인인 경우)을 등록해줍니다. 


@override
void initState() {
super.initState();
setupVoiceSDKEngine();
}

@override
void dispose() async {
await agoraEngine.leaveChannel();
super.dispose();
}

Future<void> setupVoiceSDKEngine() async {
await [Permission.microphone].request();

agoraEngine = createAgoraRtcEngine();
await agoraEngine.initialize(const RtcEngineContext(appId: appId));

agoraEngine.registerEventHandler(
RtcEngineEventHandler(
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
showMessage(
"Local user uid:${connection.localUid} joined the channel");
setState(() {
_isJoined = true;
});
},
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
showMessage("Remote user uid:$remoteUid joined the channel");
setState(() {
_remoteUid = remoteUid;
});
},
onUserOffline: (RtcConnection connection, int remoteUid,
UserOfflineReasonType reason) {
showMessage("Remote user uid:$remoteUid left the channel");
setState(() {
_remoteUid = null;
});
},
),
);
}

 

Step 11: join 메소드와 leave 메소드를 만들어줍니다. join할 때에는 먼저 ChannelMediaOptions를 만들어주어야 하는데 clientRoleType과 channelProfile을 작성해줍니다. ClientRoleType.clientRoleBroadcaster를 통해 스트림을 주고 받도록 역할을 만들어주시고 channelProfile에는 ChannelProfileType.channelProfileCommunication으로 작성합니다. channelProfileCommunication은 통화 참여자가 2명인 경우에 channelProfileLiveBroadcasting은 통화 참여자가 2명 이상인 경우 사용하면 됩니다. 그리고 joinChannel 메소드를 통해 채널에 접속하면 됩니다.

 

통화를 완료하여 통화를 종료 즉 채널에서 나가기 위해 leave 메소드를 만들어주었고 agoraEngine.leaveChannel 메소드를 통해 통화 종료를 구현할 수 있습니다. 


void join() async {
ChannelMediaOptions options = const ChannelMediaOptions(
clientRoleType: ClientRoleType.clientRoleBroadcaster,
channelProfile: ChannelProfileType.channelProfileCommunication,
);

await agoraEngine.joinChannel(
token: token,
channelId: channelName,
options: options,
uid: uid,
);
}

void leave() {
setState(() {
_isJoined = false;
_remoteUid = null;
});
agoraEngine.leaveChannel();
}

 

Step 12: UI를 표시하기 위해 Widget build 부분을 아래처럼 작성해줍니다. 

@override
Widget build(BuildContext context) {
return MaterialApp(
scaffoldMessengerKey: scaffoldMessengerKey,
home: Scaffold(
appBar: AppBar(
title: const Text('Get started with Voice Calling'),
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
children: [
//! Status
Container(
height: 40,
child: Center(
child: _status(),
),
),
//! Button Row
Row(
children: [
Expanded(
child: ElevatedButton(
child: const Text("Join"),
onPressed: () => {join()},
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
child: const Text("Leave"),
onPressed: () => {leave()},
),
),
],
),
],
),
),
);
}

Widget _status() {
String statusText;

if (!_isJoined) {
statusText = 'Join a channel';
} else if (_remoteUid == null) {
statusText = 'Waiting for a remote user to join...';
} else {
statusText = 'Connected to remote user, uid:$_remoteUid';
}
return Text(
statusText,
);
}

 

Step 13: 실기기 2개를 가지고 테스트한 모습이며 왼쪽 순서대로 처음 화면, 로컬 사용자(본인)가 Join 버튼을 눌렀을 때, 상대방이 Join 버튼을 눌렀을 때의 화면입니다. 로컬 사용자(본인)와 상대방이 모두 같은 채널에 Join하면 통화가 가능하게 됩니다.

 

 

[전체 소스 코드]

 

References:

https://docs.agora.io/en/voice-calling/get-started/get-started-sdk?platform=flutter

 

https://docs.agora.io/en/voice-calling/get-started/get-started-sdk?platform=flutter

 

docs.agora.io

 

반응형