iOS / iPadOS での利用方法

概要

Sora Flutter SDK で iOS / iPadOS アプリケーションを実装する際のカメラやマイクの権限設定、注意点について説明します。 対応バージョンは iOS 16 以降、iPadOS 16 以降です。 Sora への接続やメディアデバイスの操作といったプラットフォーム共通の Dart API については、他ページをご確認ください。

権限

iOS / iPadOS で音声や映像を配信する場合は、配信するメディアに応じてカメラまたはマイクの権限が必要になります。 音声のみの場合はマイク、映像のみの場合はカメラだけで十分です。 SDK は権限リクエストを行わないため、アプリケーション側で Info.plist の用途説明の設定とネイティブ側の権限リクエスト、および Dart 側からの呼び出しを実装してください。

Info.plist の用途説明

<key>NSCameraUsageDescription</key>
<string>カメラを利用する目的を記述します</string>
<key>NSMicrophoneUsageDescription</key>
<string>マイクを利用する目的を記述します</string>

ランタイム権限のリクエスト

ランタイム権限を取得するには、Dart 側から MethodChannel 経由でプラットフォームの権限 API を呼び出します。

Dart 側の実装例

Dart 側の ensureMediaPermissions() は MethodChannel に camera / microphone の要否を渡すラッパーです。 この呼び出しは Android / iOS / macOS で共通で利用することができます。

import 'dart:io';

import 'package:flutter/services.dart';

/// プラットフォームのメディア権限(カメラ・マイク)を要求します。
///
/// ``MethodChannel`` 経由で各プラットフォームのネイティブ権限 API を呼び出します。
/// この Dart コードは ``Android`` / ``iOS`` / ``macOS`` で共通です。
///
/// プラットフォームが ``Android`` / ``iOS`` / ``macOS`` 以外の場合は
/// 常に ``true`` を返します。
///
/// ネイティブ側では同じチャンネル名 ``"example/permissions"`` (チャンネル名は例)で
/// ``ensureMediaPermissions`` メソッドをハンドリングする必要があります。
///
/// - Android: ``ActivityCompat.requestPermissions`` で
///   ``CAMERA`` / ``RECORD_AUDIO`` を要求
/// - iOS / macOS: ``AVCaptureDevice.requestAccess`` で
///   ``.video`` / ``.audio`` を要求
///
/// [needsCamera]: カメラ権限が必要な場合に ``true``
/// [needsMicrophone]: マイク権限が必要な場合に ``true``
///
/// 戻り値: すべての要求された権限が許可された場合に ``true``
Future<bool> ensureMediaPermissions({
  required bool needsCamera,
  required bool needsMicrophone,
}) async {
  // Android / iOS / macOS 以外では権限要求の仕組みが異なるため
  // 常に許可扱いとします。
  if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) {
    return true;
  }

  // MethodChannel 経由でネイティブ側の権限 API を呼び出します。
  // チャンネル名 "example/permissions" はネイティブ側と一致させる必要があります。
  // (実際の実装時はアプリケーション名等に合わせて命名してください)
  const channel = MethodChannel('example/permissions');

  // 戻り値が null の場合は false として扱います。
  return await channel.invokeMethod<bool>(
        'ensureMediaPermissions',
        <String, Object?>{
          'camera': needsCamera,
          'microphone': needsMicrophone,
        },
      ) ??
      false;
}

接続前の呼び出し例:

final granted = await ensureMediaPermissions(
  needsCamera: true,
  needsMicrophone: true,
);
if (!granted) {
  return;
}
final stream = await MediaDevices.getUserMedia(
  const GetUserMediaOptions(audio: true, video: true),
);
final conn = await Sora.createConnection(
  SoraConnectionConfig(
    signalingUrls: <String>['wss://sora.example.com/signaling'],
    channelId: 'example-channel',
    role: SoraRole.sendrecv,
    audio: true,
    video: true,
  ),
);
await conn.connect(stream);

iOS / iPadOS ネイティブ側の実装

AppDelegate.swift で MethodChannel のハンドラを設定し、 AVCaptureDevice.requestAccess でカメラ・マイクの許可を要求します。

import AVFoundation
import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
    private var permissionChannel: FlutterMethodChannel?

    func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
        GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)

        let channel = FlutterMethodChannel(
             name: "example/permissions",
            binaryMessenger: engineBridge.applicationRegistrar.messenger()
        )
        channel.setMethodCallHandler { call, result in
            switch call.method {
            case "ensureMediaPermissions":
                let args = call.arguments as? [String: Any] ?? [:]
                let needsCamera = (args["camera"] as? Bool) ?? false
                let needsMicrophone = (args["microphone"] as? Bool) ?? false
                self.ensureMediaPermissions(
                    needsCamera: needsCamera,
                    needsMicrophone: needsMicrophone,
                    result: result
                )
            default:
                result(FlutterMethodNotImplemented)
            }
        }
        permissionChannel = channel
    }

    private func ensureMediaPermissions(
        needsCamera: Bool,
        needsMicrophone: Bool,
        result: @escaping FlutterResult
    ) {
        requestCameraIfNeeded(needsCamera) { cameraGranted in
            if !cameraGranted {
                result(false)
                return
            }
            self.requestMicrophoneIfNeeded(needsMicrophone) { microphoneGranted in
                result(microphoneGranted)
            }
        }
    }

    private func requestCameraIfNeeded(
        _ needsCamera: Bool,
        completion: @escaping (Bool) -> Void
    ) {
        guard needsCamera else {
            completion(true)
            return
        }
        let status = AVCaptureDevice.authorizationStatus(for: .video)
        switch status {
        case .authorized:
            completion(true)
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { granted in
                DispatchQueue.main.async {
                    completion(granted)
                }
            }
        case .denied, .restricted:
            completion(false)
        @unknown default:
            completion(false)
        }
    }

    private func requestMicrophoneIfNeeded(
        _ needsMicrophone: Bool,
        completion: @escaping (Bool) -> Void
    ) {
        guard needsMicrophone else {
            completion(true)
            return
        }
        let status = AVCaptureDevice.authorizationStatus(for: .audio)
        switch status {
        case .authorized:
            completion(true)
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .audio) { granted in
                DispatchQueue.main.async {
                    completion(granted)
                }
            }
        case .denied, .restricted:
            completion(false)
        @unknown default:
            completion(false)
        }
    }
}

実装の参照先

devtools の実装全体は以下を参照してください。

  • Dart: devtools/lib/src/devtools_connection_controller.dart ( ensureMediaPermissions )
  • iOS ネイティブ: devtools/ios/Runner/AppDelegate.swift

その他

  • カメラ送信を含む動作確認には実機が必要です(エミュレーターではカメラデバイスを利用できません)。音声のみの送信はエミュレーターでも確認できます
  • リリースビルドでは縮小や難読化の影響を受けるため、 release でも必ず起動・動作確認をしてください
  • カメラやマイクを使う場合は、ビルド前に Info.plist の用途説明を必ず設定してください
  • アプリケーション側で CocoaPods や Xcode 設定を独自に変更する場合は、SDK 側が前提にしているリンク設定を壊さないように注意してください