56
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FlutterでWebRTCをやってみる with AgoraSDK

Last updated at Posted at 2020-05-24

はじめに

WebRTC を使ったアプリを開発する機会があり、何かお手軽な package ないかなーと探していたところ FlutterWebRTC を手軽に利用できる plugin を見つけたので試しにアプリを作成してみました。

WebRTC とは

WebRTC (Web Real-Time Communication) とは、ビデオや音声、データをブラウザ間でやり取り可能にするための規格で、Google によってオープンソース化されました。 ユーザーはその API を経由することでリアルタイム通信を実現できます。

参考: WebRTCの基本とP2P通信が成立するまでを学ぶ

最近ではコロナの影響もあり、ウェブ会議システムやチャットツールなどの利用者が急増しています。

  • Zoom
  • Hang out
  • Discord
  • Microsoft Teams
スクリーンショット 2020-05-25 04.37.36.png

コロナをきっかけに一般に広く知られるようになり、今まで利用するに至らなかった勢が利用していました(周りでも)。
今後これらのツールが一般的に利用されるようになるのではないでしょうか。

Agora.IO SDK

Agora.IO が開発している、ビデオ通話やライブ配信を構築できる SDK です。
基盤となるこの SDK を利用して様々な言語やプラットフォームで利用することができます。
日本では NTT CommunicationsSkyWay に相当するものです。

Flutter では agora_rtc_engine | Flutter package を利用します。

実装

実際にサンプルアプリを実装していきます.

Agora Project の作成

Agora を利用するには Agora.IOProject を作成し、 AppID を入手する必要があります.
無料枠が 10000 minutes 程あるので当分は無料で利用できますし、無料枠を超えても金額を請求されることはないので安心してください.

まずはこちらから登録して Project を作成します.
Agora | Sign Up

Project を作成後、Project Management をクリックして作成した ProjectAppID をメモしておきます.

スクリーンショット 2020-05-21 18.21.36.png

Platform settings

Platform 毎に権限周りや固有の設定をしていきます.

iOS

ios/Runner/info.plist にカメラとマイクの権限を追加

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 を利用するので以下も追加

info.plist
<key>io.flutter.embedded_views_preview</key>
<true/>

Android

android/app/src/main/AndroidManifest.xml に以下の権限を追加します.

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 を作成し、以下を書いておきます
(難読化によるアプリクラッシュを防ぐ).

proguard-rules.pro
-keep class io.agora.**{*;}

platform 固有の設定は以上です.

agora_rtc_engine 周りの実装

ここがメインの部分になります.

agora_rtc_engine の追加

pubspeck.yaml
dependencies:
  agora_rtc_engine: ^1.0.12

Controller

今回は state_notifierfreezed パッケージを利用して実装しました。
agora_rtc_engine に関する処理は全部この中でやっています。

WebRtcController.dart
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 が提供されるのでそれを利用します。

webrtc_view.dart
AgoraRenderWidget(
    uid, /// Join に成功した場合に取得できる uid
    local: true, /// 自分であれば true, それ以外は false
    preview: true,
    mode: VideoRenderMode.Hidden /// object-fit か object-cover か
)

Page

webrtc_page.dart
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 の方を起動しておき、同じ RoomJoin しています。

映像の遅延もなく、動作もサクサクで快適に動作しました。
Flutter で手軽に WebRTC を利用することができるのは嬉しいですね。

終わりに

今回作成したサンプルアプリは GitHub に公開しているのでご自由にお使いください。

yukitaka13-1110 / flutter_webrtc_agora_example

コロナウイルスの影響もあってリモートワークやその他遠隔コミュニケーションでビデオ通話ができるツールがの需要が高まってきているのを感じているので、その裏側を作るのも面白そうだなと思いました。

 

56
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?