İçeriğe geç / Skip to content / Zum Inhalt

Flutter-Kamera ohne erzwungene Ausrichtung: drei Fehler und ihre Lösungen

Ahmet Balaman

Zuletzt aktualisiert:

4 Min. Lesezeit

FlutterDartcamera-PaketVideoRecapdayDebugging
Flutter-Kamera ohne erzwungene Ausrichtung: drei Fehler und ihre Lösungen

Wer in Flutter mit dem camera-Paket des Flutter-Teams einen Videobildschirm gebaut hat, kennt vermutlich mindestens einen dieser Fälle: die Vorschau ist um 90° gedreht, die Oberfläche friert beim Aufnahmestart ein, oder die Kamera kommt nicht zurück, nachdem jemand die Mitteilungszentrale heruntergezogen hat.

Beim Kamerabildschirm für Recapday hatte ich alle drei. Dieser Beitrag erklärt Ursache und Lösung für jeden Fall — und wie man einen Bildschirm baut, der niemanden zwingt, das Handy auf eine bestimmte Weise zu halten.

Zuerst das Prinzip: Ausrichtung zu erzwingen ist die falsche Lösung

In der ersten Fassung habe ich den Bildschirm mit der in der API-Referenz zu SystemChrome.setPreferredOrientations beschriebenen Methode ins Querformat gesperrt, damit Videos horizontal werden. Klassischer Griff, und falsch.

Falsch aus einem einfachen Grund: die Person weiß selbst, wie sie ihr Handy hält. Wer das Handy zückt, um einen Moment einzufangen, und dann "erst drehen" liest, verpasst genau diesen Moment.

Das richtige Modell:

  • Oberfläche im Hochformat fixiert. Knöpfe bleiben, wo sie sind, das Layout springt nicht.
  • Kamera frei. Hoch- oder querformatig — die Ausrichtung schreibt der Sensor des Geräts ohnehin korrekt in die Datei.
  • Der Zusammenschnitt akzeptiert beides. Clips unterschiedlicher Seitenverhältnisse treffen sich in einem Vlog.

Dieses Prinzip machte auch die folgenden Fehler leichter lösbar, weil ich die Kamera nicht mehr in eine Form zwang.

Fehler 1: die Vorschau ist um 90° gedreht

Symptom: CameraPreview zeigt das Bild seitlich.

Das Querformat-Verhältnis des camera-Pakets wird für ein Hochformat invertiert; erzwungene Ausrichtung lässt die Vorschau seitlich wirken

Ursache: weil ich die Ausrichtung erzwang, widersprachen sich Flutters Layout und die Sensorausrichtung der Kamera. CameraPreview meldet aspectRatio relativ zur natürlichen Ausrichtung des Geräts; das erzwungene Drehen ließ beide auseinanderlaufen.

Lösung: die Sperre entfernen und die Vorschau aus buildPreview() des Controllers aufbauen, mit invertiertem Verhältnis:

// Hochformatrahmen: das camera-Paket meldet aspectRatio gegen die
// natürliche (querformatige) Ausrichtung des Geräts, wir setzen es in
// eine hochformatige Box.
final oran = 1 / k.value.aspectRatio;

return AspectRatio(
  aspectRatio: oran,
  child: ClipRRect(
    borderRadius: BorderRadius.circular(24),
    child: k.buildPreview(),
  ),
);

Statt CameraPreview buildPreview() zu nutzen ist wichtig: CameraPreview wendet intern eigene Drehlogik an und drehte zusammen mit der erzwungenen Ausrichtung doppelt.

Fehler 2: die Oberfläche friert beim Aufnahmestart ein

Symptom: der Aufruf von startVideoRecording() blockiert die App ein bis zwei Sekunden. Kein Ruckeln — ein sichtbares Einfrieren.

Ursache: wird eine Aufnahme auf einer Session verlangt, die nicht auf Video vorbereitet ist, konfiguriert die Plattform die Session in diesem Moment um. Diese Arbeit blockiert den Haupt-Thread.

Lösung: die Vorbereitung direkt nach der Initialisierung erledigen, bevor jemand den Auslöser berührt:

final yeni = CameraController(
  kamera,
  ResolutionPreset.high,
  enableAudio: true,
);

await yeni.initialize();

// Ohne diese Zeile friert die erste Aufnahme 1-2 Sekunden ein:
// die Session bereitet sich erst zum Aufnahmezeitpunkt vor.
await yeni.prepareForVideoRecording();

Eine Zeile. Das Einfrieren war vollständig weg.

Fehler 3: die Kamera stirbt nach der Mitteilungszentrale

Symptom: jemand wischt die Mitteilungszentrale herunter und schließt sie wieder — die Vorschau bleibt schwarz.

Ursache: mein Lifecycle-Handler gab den Controller auch bei AppLifecycleState.inactive frei. Wie die Dokumentation zum Enum AppLifecycleState erklärt, heißt inactive aber nicht "im Hintergrund"; Mitteilungszentrale, Kontrollzentrum, ein eingehender Anruf und der App-Umschalter lösen es alle aus. Ich baute die Kamera ab — und weil resumed nicht immer folgt, nie wieder auf.

Lösung: nur in Zuständen freigeben, die wirklich Hintergrund bedeuten:

@override
void didChangeAppLifecycleState(AppLifecycleState durum) {
  final k = _kontrolcu;
  if (k == null || !k.value.isInitialized) return;

  switch (durum) {
    // Wirklich im Hintergrund: Kamera freigeben.
    case AppLifecycleState.paused:
    case AppLifecycleState.hidden:
    case AppLifecycleState.detached:
      _kapat();

    case AppLifecycleState.resumed:
      _baslat();

    // inactive: Mitteilungszentrale, Kontrollzentrum, eingehender Anruf.
    // Kamera muss leben bleiben; sonst bleibt ein schwarzer Bildschirm.
    case AppLifecycleState.inactive:
      break;
  }
}

inactive als eigenen leeren case zu schreiben ist besser als ein default: kommt ein neuer Zustand dazu, warnt der Compiler.

Der Auslöser: Tippen und Halten, beides

Statt zu raten, wie jemand aufnimmt, habe ich beides unterstützt. Tipp- und Langdruck-Gesten auf demselben Knopf:

GestureDetector(
  // Einmal tippen: nimmt einen Clip fester Länge auf.
  onTap: _kayitta ? null : _cek,

  // Gedrückt halten: nimmt auf, bis der Finger geht.
  onLongPressStart: (d) {
    _basmaBaslangiciY = d.globalPosition.dy;
    _kayitBaslat();
  },
  onLongPressMoveUpdate: _basiliZoom,
  onLongPressEnd: (_) => _kayitBitir(),

  child: _deklansor(),
)

onLongPressMoveUpdate misst, wie weit der Finger vom Startpunkt nach oben gewandert ist, und übersetzt das in Zoom. Ein Detail: manche Telefone melden einen Maximalzoom über 100, und dort ist das Bild unbrauchbar. Die Obergrenze habe ich selbst gesetzt:

double get _ustZoom => _maxZoom > 8 ? 8 : _maxZoom;

Auch der Kamerawechsel während der Aufnahme wird unterstützt, per Doppeltippen. Anhalten und neu starten ist nicht nötig: auf den meisten Geräten tauscht setDescription den Stream, ohne die Aufnahme zu unterbrechen.

Zusammengefasst

Alle drei Fehler haben dieselbe Ursache: gegen das Verhalten der Plattform zu arbeiten. Die erzwungene Ausrichtung kollidierte mit der Drehung, die verschobene Vorbereitung brachte das Einfrieren, eine zu weite Lesart des Lifecycles tötete die Kamera.

Eine kurze Checkliste:

  1. Ausrichtung nicht erzwingen. Oberfläche fixieren, Kamera freilassen.
  2. Nach initialize() prepareForVideoRecording() aufrufen.
  3. Die Kamera nicht bei inactive freigeben.
  4. Den Zoom selbst begrenzen.
  5. Die Vorschau aus buildPreview() bauen und das Verhältnis für den Hochformatrahmen invertieren.

Die gesamte Architektur von Recapday habe ich in einem eigenen Beitrag beschrieben.

Häufig gestellte Fragen

Warum ist eine erzwungene Ausrichtung im Kamerabildschirm falsch?

Sie trennt das Layout-System von Flutter von der Sensorausrichtung der Kamera. CameraPreview meldet sein Verhältnis zur natürlichen Ausrichtung des Geräts, und sobald beide auseinanderdriften, wirkt die Vorschau seitlich.

Was tun, wenn die Vorschau um 90 Grad gedreht erscheint?

Die erzwungene Ausrichtung entfernen und das Verhältnis invertieren: 1 / controller.value.aspectRatio an ein AspectRatio-Widget geben und die Vorschau aus dem buildPreview() des Controllers aufbauen.

Wie wird das Einfrieren beim Start der Aufnahme behoben?

Rufen Sie direkt nach initialize() die Methode prepareForVideoRecording() auf. Ohne diese Zeile bereitet die Sitzung die Aufnahme erst im Moment des Drucks vor und blockiert den Haupt-Thread für ein bis zwei Sekunden.

Warum bleibt die Kamera nach dem Öffnen der Mitteilungszentrale schwarz?

Weil der Controller bei AppLifecycleState.inactive freigegeben wird. Dieser Zustand bedeutet nicht, dass die App in den Hintergrund ging: Mitteilungszentrale, Kontrollzentrum, ein eingehender Anruf und der App-Umschalter lösen ihn ebenfalls aus. Geben Sie die Kamera nur bei paused und hidden frei.

Kommentare