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

Flutter'da Kamerayı Kullanıcıya Bırakmak: Yön Zorlamadan Video Çekmek

Ahmet Balaman
FlutterDartKameracamera paketiVideoAnilogMobil GeliştirmeHata Ayıklama

Flutter'da camera paketiyle çalışan bir video ekranı yazdıysanız muhtemelen şu üçünden en az birine çarptınız: önizleme 90 derece ters duruyor, kayda basınca arayüz donuyor, ya da bildirim merkezini açıp kapatınca kamera bir daha açılmıyor.

Anılog için kamera ekranını yazarken üçünü de yaşadım. Bu yazıda her birinin sebebini ve çözümünü anlatıyorum — ayrıca kullanıcıyı telefonu şu ya da bu şekilde tutmaya zorlamadan çalışan bir ekranın nasıl kurulduğunu.

Önce ilke: yön zorlamak yanlış çözüm

İlk sürümde video yatay olsun diye ekranı setPreferredOrientations ile yatay moda kilitliyordum. Klasik çözüm, ve yanlış.

Yanlış olmasının sebebi basit: kullanıcı telefonu nasıl tutacağını kendisi bilir. Bir anı yakalamak için telefonu çıkaran kişiyi "önce yan çevir" diye durdurmak, yakalamak istediği anı kaçırtıyor.

Doğru model şu:

  • Arayüz dikey sabit. Düğmeler yerinde kalıyor, düzen bozulmuyor.
  • Kamera serbest. Kullanıcı dikey de çekebilir, yatay da. Cihazın kendi sensörü zaten doğru yönü videoya gömüyor.
  • Birleştirme tarafı ikisini de kabul ediyor. Farklı oranlardaki klipler tek vlogta buluşuyor.

Bu ilkeyi benimseyince aşağıdaki hataların çözümü de kolaylaştı, çünkü artık kamerayı zorla bir yere oturtmaya çalışmıyordum.

Hata 1: Önizleme 90 derece ters

Belirti: CameraPreview widget'ı görüntüyü yan gösteriyor.

Sebep: yönü zorladığım için Flutter'ın düzen sistemiyle kameranın kendi sensör yönü çakışıyordu. CameraPreview, aspectRatio değerini cihazın doğal yönüne göre veriyor; ben ekranı zorla döndürünce ikisi ayrışıyordu.

Çözüm: yön zorlamayı kaldırmak, ve önizlemeyi kontrolcünün kendi buildPreview() çıktısıyla, oranı ters çevirerek kurmak:

// Dikey çerçeve: camera paketi aspectRatio'yu cihazın doğal (yatay)
// yönüne göre verir, biz dikey bir kutuya oturtuyoruz.
final oran = 1 / k.value.aspectRatio;

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

CameraPreview yerine buildPreview() kullanmak önemli: CameraPreview içeride kendi döndürme mantığını uyguluyor ve zorlanmış yönle birlikte iki kez döndürme yapıyordu.

Hata 2: Kayda basınca arayüz donuyor

Belirti: startVideoRecording() çağrısında uygulama 1-2 saniye tamamen kilitleniyor. Küçük bir takılma değil, gözle görülür bir donma.

Sebep: kamera oturumu video kaydına hazır değilken kayıt istendiğinde, platform tarafı oturumu o anda yeniden yapılandırıyor. Bu iş ana iş parçacığını bloke ediyor.

Çözüm: kontrolcü başlatıldıktan hemen sonra, kullanıcı daha düğmeye basmadan hazırlığı yaptırmak:

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

await yeni.initialize();

// Bu satır olmadan ilk kayıtta 1-2 saniyelik donma oluyor:
// oturum kayda hazırlığı kayıt anında yapıyor.
await yeni.prepareForVideoRecording();

Tek satır. Donmanın tamamı gitti.

Hata 3: Bildirim merkezi açılınca kamera ölüyor

Belirti: kullanıcı ekranın üstünden aşağı çekip bildirim merkezini açıyor, kapatıyor — kamera siyah kalıyor.

Sebep: yaşam döngüsü dinleyicimde kontrolcüyü AppLifecycleState.inactive durumunda da kapatıyordum. Oysa inactive, uygulamanın arka plana gitmesi değil; bildirim merkezi, denetim merkezi, gelen arama bildirimi ve uygulama değiştirici hep bu durumu tetikliyor. Kamerayı kapatıyordum ama resumed her zaman gelmediği için bir daha açmıyordum.

Çözüm: yalnızca gerçekten arka plana giden durumlarda kapatmak:

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

  switch (durum) {
    // Gerçekten arka plandayız: kamerayı bırak.
    case AppLifecycleState.paused:
    case AppLifecycleState.hidden:
    case AppLifecycleState.detached:
      _kapat();

    case AppLifecycleState.resumed:
      _baslat();

    // inactive: bildirim merkezi, denetim merkezi, gelen arama.
    // Kamera açık kalmalı; kapatırsak geri dönünce siyah ekran kalıyor.
    case AppLifecycleState.inactive:
      break;
  }
}

inactive durumunu ayrı bir case olarak yazıp boş bırakmak, default kullanmaktan daha iyi: yeni bir durum eklendiğinde derleyici uyarıyor.

Deklanşör: dokunma ve basılı tutma birlikte

Kullanıcının nasıl çekeceğini tahmin etmek yerine iki yolu da destekledim. Aynı düğmede hem onTap hem uzun basma jestleri:

GestureDetector(
  // Tek dokunuş: sabit süreli klip çeker.
  onTap: _kayitta ? null : _cek,

  // Basılı tutma: parmak kalkana kadar kaydeder.
  onLongPressStart: (d) {
    _basmaBaslangiciY = d.globalPosition.dy;
    _kayitBaslat();
  },
  onLongPressMoveUpdate: _basiliZoom,
  onLongPressEnd: (_) => _kayitBitir(),

  child: _deklansor(),
)

onLongPressMoveUpdate ile parmağın başlangıç noktasından ne kadar yukarı kaydığını ölçüp yakınlaştırmaya çeviriyorum. Bir ayrıntı: cihazın bildirdiği en yüksek yakınlaştırma bazı telefonlarda 100'ün üzerinde çıkıyor ve o seviyede görüntü kullanılamaz hâle geliyor. Üst sınırı kendim koydum:

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

Kayıt sürerken kamerayı çevirmek de destekleniyor — çift dokunuş. Bunun için kaydı durdurup yeniden başlatmak gerekmiyor; setDescription çağrısı çoğu cihazda kaydı bölmeden çalışıyor.

Toparlarsak

Üç hatanın ortak noktası şu: platformun kendi davranışıyla çakışmak. Yönü zorlayınca döndürme çakıştı, hazırlığı ertelemekle donma geldi, yaşam döngüsünü fazla geniş yorumlayınca kamera öldü.

Kısa kontrol listesi:

  1. Yönü zorlamayın. Arayüzü sabitleyin, kamerayı serbest bırakın.
  2. initialize() sonrası prepareForVideoRecording() çağırın.
  3. Yaşam döngüsünde inactive durumunda kamerayı kapatmayın.
  4. Yakınlaştırmaya kendi üst sınırınızı koyun.
  5. Önizlemeyi buildPreview() ile kurun ve oranı dikey çerçeveye çevirin.

Anılog'un mimarisinin tamamını ayrı bir yazıda anlattım.

Yorumlar