Android での利用方法¶
概要¶
Sora Flutter SDK で Android アプリケーションを実装する際のカメラやマイクの権限設定、注意点について説明します。 対応バージョンは Android 10 (API 29) 以降です。 Sora への接続やメディアデバイスの操作といったプラットフォーム共通の Dart API については、他ページをご確認ください。
動作環境¶
- Android 10 (API 29) 以降 (arm64-v8a)
- Flutter 3.44.0 以上
- Android SDK / Android NDK 28.2.13676358
- Sora 2025.2.0 以降
依存関係¶
Android 向けビルドでは、Gradle の fetchNativeDeps タスクが libwebrtc-c と webrtc の配布物を自動取得します。
取得対象のバージョン、URL、SHA-256 は scripts/native_deps.json で管理しています。
ビルド¶
アプリケーションのディレクトリで依存関係を取得し、APK をビルドします。
flutter pub get
flutter build apk --debug
flutter build apk --release
権限¶
Android で音声や映像を配信する場合は、配信するメディアに応じてカメラまたはマイクの権限が必要になります。
音声のみの場合は RECORD_AUDIO 、映像のみの場合は CAMERA だけで十分です。
CAMERA と RECORD_AUDIO はランタイム権限で、マニフェストへの宣言だけでは付与されません。
また Sora への接続には INTERNET 権限が必要です。こちらはインストール時に自動で付与される通常の権限のため、マニフェストへの宣言のみで十分です。
SDK は権限リクエストを行わないため、アプリケーション側で AndroidManifest.xml の宣言とネイティブ側の権限リクエスト、および Dart 側からの呼び出しを実装してください。
AndroidManifest.xml の宣言¶
シグナリング接続用の INTERNET と、配信用の CAMERA / RECORD_AUDIO を宣言します。
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
ランタイム権限のリクエスト¶
ランタイム権限を取得するには、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);
Android ネイティブ側の実装例¶
MainActivity.kt で MethodChannel のハンドラを設定し、
ActivityCompat.requestPermissions でカメラ・マイクの許可を要求します。
import android.Manifest
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private var permissionChannel: MethodChannel? = null
private var pendingPermissionResult: MethodChannel.Result? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
permissionChannel =
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"example/permissions",
).also { channel ->
channel.setMethodCallHandler { call, result ->
when (call.method) {
"ensureMediaPermissions" -> {
val args = call.arguments as? Map<*, *> ?: emptyMap<String, Any>()
val needsCamera = (args["camera"] as? Boolean) ?: false
val needsMicrophone = (args["microphone"] as? Boolean) ?: false
ensureMediaPermissions(
needsCamera = needsCamera,
needsMicrophone = needsMicrophone,
result = result,
)
}
else -> result.notImplemented()
}
}
}
}
private fun ensureMediaPermissions(
needsCamera: Boolean,
needsMicrophone: Boolean,
result: MethodChannel.Result,
) {
val permissions = mutableListOf<String>()
if (needsCamera &&
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
) {
permissions.add(Manifest.permission.CAMERA)
}
if (needsMicrophone &&
ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
) {
permissions.add(Manifest.permission.RECORD_AUDIO)
}
if (permissions.isEmpty()) {
result.success(true)
return
}
pendingPermissionResult = result
ActivityCompat.requestPermissions(
this,
permissions.toTypedArray(),
REQUEST_MEDIA_PERMISSIONS,
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode != REQUEST_MEDIA_PERMISSIONS) {
return
}
val result = pendingPermissionResult ?: return
pendingPermissionResult = null
result.success(grantResults.all { it == PackageManager.PERMISSION_GRANTED })
}
companion object {
private const val REQUEST_MEDIA_PERMISSIONS = 1001
}
}
実装の参照先¶
devtools の実装全体は以下を参照してください。
- Dart:
devtools/lib/src/devtools_connection_controller.dart(ensureMediaPermissions) - Android ネイティブ:
devtools/android/app/src/main/kotlin/com/example/devtools/MainActivity.kt
その他¶
- カメラ送信を含む動作確認には実機が必要です(エミュレーターではカメラデバイスを利用できません)。音声のみの送信はエミュレーターでも確認できます
- リリースビルドでは縮小や難読化の影響を受けるため、
releaseでも必ず起動・動作確認をしてください - R8 や ProGuard の設定を独自に上書きする場合は、SDK が前提にしている keep 設定を壊さないように注意してください。JNI 初期化に必要なクラスが削除または難読化されると、起動直後にクラッシュする可能性があります。詳細は Android の ネイティブ ライブラリの最適化 をご確認ください
- NDK や Gradle 周りを大きく変更する場合は、ネイティブライブラリの読み込み条件が変わらないか確認してください