Bernds Anruftest Handy↔PC (2026-10-07): - Teilen am Handy: App "verkleinerte sich", Anruf danach weg. Ursache: Android 14+ verlangt vor MediaProjection einen Vordergrunddienst vom Typ mediaProjection; flutter_webrtc startet keinen, der Manifest-Eintrag verwies auf eine nicht mehr existierende Klasse. Neu: CallService (microphone|mediaProjection) + CallForeground: Zustimmung → Dienst (wartet auf echten startForeground) → Mitschnitt. Emulator-Test (Android 16): mit Fix 1080x2400@30 läuft; Gegenprobe ohne Dienst wirft genau 'Media projections require a foreground service ...'. - Dienst läuft während jedes Anrufs: Mikrofon bleibt im Hintergrund aktiv, Benachrichtigung "Anruf läuft" führt zurück. Mikrofon-Typ nur mit Berechtigung, sonst schmalere Variante statt Dienst-Abbruch. - Handy hörte nichts: SDK schaltet Sprachanrufe auf die Hörmuschel. Jetzt Lautsprecher als Standard (wie Discord) + Umschalter in der Anrufleiste. - LiveKit-Teilen am Handy öffnete den PC-Quellen-Auswähler → System-Dialog. - Im offenen Chat (Handy) Leiste "Anruf läuft · Zurück". - Einstellungen → Sprache & Video: Mikrofontest mit Pegel (PC-Diagnose: das Standardgerät lieferte nur Stille – Voicemeeter/virtuelle Geräte). Co-Authored-By: Claude Opus 5.5 <[email protected]>
85 lines
3.0 KiB
Dart
85 lines
3.0 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||
|
||
/// Anruf-Vordergrunddienst auf Android (android/.../CallService.kt).
|
||
///
|
||
/// * Solange ein Anruf läuft, hält der Dienst das Mikrofon auch im
|
||
/// Hintergrund aktiv (Android schaltet es sonst stumm) und zeigt eine
|
||
/// „Anruf läuft“-Benachrichtigung, über die man zurück in die App kommt.
|
||
/// * Für eine Bildschirmübertragung MUSS ab Android 14 vorher ein Dienst vom
|
||
/// Typ mediaProjection laufen – und der darf erst NACH der Zustimmung des
|
||
/// Nutzers starten. [prepareScreenCapture] erledigt beides in der richtigen
|
||
/// Reihenfolge; flutter_webrtc benutzt danach die gespeicherte Zustimmung.
|
||
///
|
||
/// Beide Anrufwege (1:1 über Matrix, Sprachkanäle über LiveKit) melden sich
|
||
/// mit eigenem Namen an; der Dienst endet erst, wenn keiner mehr läuft.
|
||
/// Auf anderen Plattformen passiert nichts.
|
||
class CallForeground {
|
||
CallForeground._();
|
||
|
||
static const _channel = MethodChannel('chat.pyramid.pyramid/call');
|
||
static final _owners = <String>{};
|
||
static final _screenOwners = <String>{};
|
||
static String _title = 'Pyramid';
|
||
|
||
static bool get _android => !kIsWeb && Platform.isAndroid;
|
||
|
||
/// true = Dienst läuft (bzw. wurde beendet) wie gewünscht.
|
||
static Future<bool> _apply() async {
|
||
if (!_android) return true;
|
||
try {
|
||
if (_owners.isEmpty) {
|
||
await _channel.invokeMethod('stop');
|
||
return true;
|
||
}
|
||
final ok = await _channel.invokeMethod<bool>('start', {
|
||
'title': _title,
|
||
'screen': _screenOwners.isNotEmpty,
|
||
});
|
||
return ok ?? false;
|
||
} catch (e) {
|
||
debugPrint('[CallForeground] $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// Anruf von [owner] läuft (z. B. 'voip', 'livekit').
|
||
static Future<void> callStarted(String owner, {required String title}) async {
|
||
_owners.add(owner);
|
||
_title = title;
|
||
await _apply();
|
||
}
|
||
|
||
/// Anruf von [owner] ist beendet.
|
||
static Future<void> callEnded(String owner) async {
|
||
final changed = _owners.remove(owner) | _screenOwners.remove(owner);
|
||
if (changed) await _apply();
|
||
}
|
||
|
||
/// Vor einer Bildschirmübertragung auf Android: Zustimmung einholen, dann
|
||
/// den Dienst um „mediaProjection“ erweitern. false = Nutzer hat abgelehnt.
|
||
static Future<bool> prepareScreenCapture(String owner) async {
|
||
if (!_android) return true;
|
||
final granted = await rtc.Helper.requestCapturePermission();
|
||
if (!granted) return false;
|
||
_owners.add(owner);
|
||
_screenOwners.add(owner);
|
||
if (!await _apply()) {
|
||
// Ohne laufenden mediaProjection-Dienst würde der Mitschnitt die App
|
||
// abstürzen lassen – dann lieber gar nicht übertragen.
|
||
_screenOwners.remove(owner);
|
||
await _apply();
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// Übertragung von [owner] beendet – Dienst wieder ohne mediaProjection.
|
||
static Future<void> screenCaptureStopped(String owner) async {
|
||
if (_screenOwners.remove(owner)) await _apply();
|
||
}
|
||
}
|