はじめに
WebRTC
を使ったアプリを開発する機会があり、何かお手軽な package
ないかなーと探していたところ Flutter
で WebRTC
を手軽に利用できる plugin
を見つけたので試しにアプリを作成してみました。
WebRTC とは
WebRTC
(Web Real-Time Communication) とは、ビデオや音声、データをブラウザ間でやり取り可能にするための規格で、Google
によってオープンソース化されました。 ユーザーはその API
を経由することでリアルタイム通信を実現できます。
最近ではコロナの影響もあり、ウェブ会議システムやチャットツールなどの利用者が急増しています。
- Zoom
- Hang out
- Discord
- Microsoft Teams
コロナをきっかけに一般に広く知られるようになり、今まで利用するに至らなかった勢が利用していました(周りでも)。
今後これらのツールが一般的に利用されるようになるのではないでしょうか。
Agora.IO SDK
Agora.IO
が開発している、ビデオ通話やライブ配信を構築できる SDK
です。
基盤となるこの SDK
を利用して様々な言語やプラットフォームで利用することができます。
日本では NTT Communications
の SkyWay
に相当するものです。
Flutter
では agora_rtc_engine | Flutter package を利用します。
実装
実際にサンプルアプリを実装していきます.
Agora Project
の作成
Agora
を利用するには Agora.IO
に Project
を作成し、 AppID
を入手する必要があります.
無料枠が 10000 minutes
程あるので当分は無料で利用できますし、無料枠を超えても金額を請求されることはないので安心してください.
まずはこちらから登録して Project
を作成します.
Agora | Sign Up
Project
を作成後、Project Management
をクリックして作成した Project
の AppID
をメモしておきます.
Platform settings
Platform
毎に権限周りや固有の設定をしていきます.
iOS
ios/Runner/info.plist
にカメラとマイクの権限を追加
<key>NSCameraUsageDescription</key>
<string>Use camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>Use mic</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
WebView
を利用するので以下も追加
<key>io.flutter.embedded_views_preview</key>
<true/>
Android
android/app/src/main/AndroidManifest.xml
に以下の権限を追加します.
<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.ACCESS_NETWORK_STATE" />
android/app/src/proguard-rules.pro
を作成し、以下を書いておきます
(難読化によるアプリクラッシュを防ぐ).
-keep class io.agora.**{*;}
platform
固有の設定は以上です.
agora_rtc_engine
周りの実装
ここがメインの部分になります.
agora_rtc_engine
の追加
dependencies:
agora_rtc_engine: ^1.0.12
Controller
今回は state_notifier
と freezed
パッケージを利用して実装しました。
agora_rtc_engine
に関する処理は全部この中でやっています。
import 'package:agora_example/models/entities/entities.dart';
import 'package:agora_example/utils/constants.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:state_notifier/state_notifier.dart';
import 'webrtc_state.dart';
class WebRtcController extends StateNotifier<WebRtcState> {
WebRtcController({
@required String roomName,
}) : super(const WebRtcState()) {
debugPrint('$tag: init()');
initWebRtc(roomName: roomName);
}
static const tag = 'WebRtcController';
Future<void> initWebRtc({@required String roomName}) async {
/// initialize する前に必ず Permission Request を行う
await Permission.camera.request();
await Permission.microphone.request();
/// 先程メモした AppID を利用
await AgoraRtcEngine.create(Constants.appId);
/// AV 周りに関する設定
await AgoraRtcEngine.enableAudio();
await AgoraRtcEngine.enableVideo();
await AgoraRtcEngine.setChannelProfile(ChannelProfile.Communication);
await AgoraRtcEngine.enableWebSdkInteroperability(true);
/// Event Listener を設定する
/// 自分が Join に成功した時
AgoraRtcEngine.onJoinChannelSuccess = _onJoinChannelSuccess;
/// 相手が Join に成功した時
AgoraRtcEngine.onUserJoined = _onUserJoined;
/// 自分が Leave した時
AgoraRtcEngine.onLeaveChannel = _onLeaveChannel;
/// 相手が Offline になった時
AgoraRtcEngine.onUserOffline = _onUserOffline;
/// Join する処理
await AgoraRtcEngine.startPreview();
await AgoraRtcEngine.joinChannel(null, roomName, null, 0);
}
Future<void> toggleLocalAudio() async {
final localAvStatus = state.localAvStatus;
await AgoraRtcEngine.muteLocalAudioStream(localAvStatus.mic);
state = state.copyWith(
localAvStatus: localAvStatus.copyWith(
mic: !localAvStatus.mic,
),
);
}
Future<void> toggleLocalVideo() async {
final localAvStatus = state.localAvStatus;
await AgoraRtcEngine.muteLocalVideoStream(localAvStatus.video);
state = state.copyWith(
localAvStatus: localAvStatus.copyWith(
video: !localAvStatus.video,
),
);
}
void switchView(int viewIndex) {
state = state.copyWith(viewIndex: viewIndex);
}
void _onJoinChannelSuccess(String roomName, int uid, int elapsed) {
debugPrint('$tag: onJoinChannelSuccess -> $uid');
final users = [...state.users, WebRtcUser(uid: uid)];
state = state.copyWith(
users: users,
);
}
void _onUserJoined(int uid, int elapsed) {
debugPrint('$tag: onUserJoined -> $uid');
final users = [...state.users, WebRtcUser(uid: uid)];
state = state.copyWith(
users: users,
);
}
void _onLeaveChannel() {
debugPrint('$tag: onLeaveChannel');
state = state.copyWith(users: []);
}
void _onUserOffline(int uid, int reason) {
debugPrint('$tag: onUserOffline -> $uid');
final users = <WebRtcUser>[];
for (final user in state.users) {
if (user.uid != uid) {
users.add(user);
}
}
state = state.copyWith(users: users, viewIndex: 0);
}
@override
void dispose() {
super.dispose();
/// agora_rtc_engine の破棄
AgoraRtcEngine.leaveChannel();
AgoraRtcEngine.stopPreview();
AgoraRtcEngine.destroy();
}
}
View
Agora
からは PlatformView
が提供されるのでそれを利用します。
AgoraRenderWidget(
uid, /// Join に成功した場合に取得できる uid
local: true, /// 自分であれば true, それ以外は false
preview: true,
mode: VideoRenderMode.Hidden /// object-fit か object-cover か
)
Page
import 'package:agora_example/pages/webrtc_page/room_user_list.dart';
import 'package:agora_example/pages/webrtc_page/webrtc_view.dart';
import 'package:agora_example/pages/webrtc_page/call_action_button.dart';
import 'package:agora_example/models/models.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
class WebRtcPage extends StatelessWidget {
const WebRtcPage._({Key key}) : super(key: key);
static Widget wrapped({@required String roomName}) {
return MultiProvider(
providers: [
StateNotifierProvider<WebRtcController, WebRtcState>(
create: (context) => WebRtcController(roomName: roomName),
),
],
child: const WebRtcPage._(),
);
}
@override
Widget build(BuildContext context) {
final localAvStatus = context.select(
(WebRtcState state) => state.localAvStatus,
);
return Scaffold(
appBar: null,
body: Stack(
children: [
Column(
children: [
SizedBox(
height: MediaQuery.of(context).size.height / 2,
child: const WebRtcView(),
),
SizedBox(
height: MediaQuery.of(context).size.height / 2,
child: const RoomUserList(),
),
],
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CallActionButton(
tag: 'mic mute',
icon: localAvStatus.mic ? Icons.mic : Icons.mic_off,
color: Colors.black,
backgroundColor: Colors.white,
onPressed: () {
context.read<WebRtcController>().toggleLocalAudio();
},
),
CallActionButton(
tag: 'call end',
icon: Icons.call_end,
backgroundColor: Colors.red,
onPressed: Navigator.of(context).pop,
),
CallActionButton(
tag: 'video mute',
icon: localAvStatus.video
? Icons.videocam
: Icons.videocam_off,
color: Colors.black,
backgroundColor: Colors.white,
onPressed: () {
context.read<WebRtcController>().toggleLocalVideo();
},
),
],
),
),
)
],
),
);
}
}
Demo
予め iPad
の方を起動しておき、同じ Room
に Join
しています。
映像の遅延もなく、動作もサクサクで快適に動作しました。
Flutter
で手軽に WebRTC
を利用することができるのは嬉しいですね。
終わりに
今回作成したサンプルアプリは GitHub に公開しているのでご自由にお使いください。
yukitaka13-1110 / flutter_webrtc_agora_example
コロナウイルスの影響もあってリモートワークやその他遠隔コミュニケーションでビデオ通話ができるツールがの需要が高まってきているのを感じているので、その裏側を作るのも面白そうだなと思いました。