Architektura nagrywania filmów w aplikacji CameraX

System rejestrujący zwykle nagrywa strumienie wideo i audio, kompresuje je, multipleksuje oba strumienie, a następnie zapisuje wynikowy strumień na dysku.

schemat koncepcyjny systemu rejestrowania obrazu i dźwięku;
Rysunek 1. Schemat koncepcyjny systemu przechwytywania obrazu i dźwięku.

W CameraX rozwiązaniem do nagrywania filmów jest przypadek użycia VideoCapture:

schemat koncepcyjny pokazujący, jak CameraX obsługuje przypadek użycia związany z nagrywaniem filmów;
Rysunek 2. Schemat koncepcyjny pokazujący, jak CameraX obsługuje VideoCaptureprzypadek użycia
.

Jak widać na ilustracji 2, przechwytywanie wideo w CameraX obejmuje kilka komponentów architektury wysokiego poziomu:

  • SurfaceProvider dla źródła wideo.
  • AudioSource – źródło dźwięku.
  • 2 kodery do kodowania i kompresowania obrazu i dźwięku.
  • Muxer multimedialny do multipleksowania dwóch strumieni.
  • Moduł zapisywania plików do zapisywania wyniku.

Interfejs VideoCapture API abstrahuje złożony silnik przechwytywania i udostępnia aplikacjom znacznie prostszy i bardziej przejrzysty interfejs API.

Omówienie interfejsu VideoCapture API

VideoCapture to przypadek użycia CameraX, który dobrze sprawdza się samodzielnie lub w połączeniu z innymi przypadkami użycia. Konkretne obsługiwane kombinacje zależą od możliwości sprzętowych aparatu, ale PreviewVideoCapture to prawidłowa kombinacja w przypadku wszystkich urządzeń.

Interfejs VideoCapture API składa się z tych obiektów, które komunikują się z aplikacjami:

  • VideoCapture to klasa przypadku użycia najwyższego poziomu. VideoCapture wiąże się z LifecycleOwner za pomocą CameraSelector i innych funkcji CameraX UseCases. Więcej informacji o tych koncepcjach i sposobach użycia znajdziesz w artykule Architektura CameraX.
  • Recorder to implementacja VideoOutput ściśle powiązana z VideoCapture. Aplikacja Recorder służy do rejestrowania obrazu i dźwięku. Aplikacja tworzy nagrania z Recorder.
  • PendingRecording konfiguruje nagrywanie, udostępniając opcje takie jak włączanie dźwięku i ustawianie odbiornika zdarzeń. Aby utworzyć PendingRecording, musisz użyć Recorder. PendingRecording nie nagrywa niczego.
  • Recording wykonuje właściwe nagranie. Aby utworzyć Recording, musisz użyć PendingRecording.

Rysunek 3 przedstawia relacje między tymi obiektami:

diagram przedstawiający interakcje, które występują w przypadku nagrywania filmów;
Rysunek 3. Diagram przedstawiający interakcje zachodzące w przypadku użycia funkcji VideoCapture.

Legenda:

  1. Utwórz Recorder za pomocą QualitySelector.
  2. Skonfiguruj Recorder za pomocą jednego z tych OutputOptions.
  3. W razie potrzeby włącz dźwięk, klikając withAudioEnabled().
  4. Zadzwoń pod numer start() z VideoRecordEvent słuchaczem, aby rozpocząć nagrywanie.
  5. Użyj pause()/resume()/stop() na Recording, aby sterować nagrywaniem.
  6. Odpowiadaj na VideoRecordEvents w detektorze zdarzeń.

Szczegółową listę interfejsów API znajdziesz w pliku current.txt w kodzie źródłowym.

Korzystanie z interfejsu VideoCapture API

Aby zintegrować przypadek użycia VideoCapture CameraX z aplikacją, wykonaj te czynności:

  1. Powiąż VideoCapture.
  2. Przygotowywanie i konfigurowanie nagrywania.
  3. Rozpocznij i kontroluj nagrywanie czasu działania.

W sekcjach poniżej znajdziesz informacje o tym, co możesz zrobić na każdym etapie, aby uzyskać kompleksową sesję nagrywania.

Powiąż VideoCapture

Aby powiązać przypadek użycia VideoCapture, wykonaj te czynności:

  1. Utwórz obiekt Recorder.
  2. Utwórz obiekt VideoCapture.
  3. Połącz z Lifecycle.

Interfejs CameraX VideoCapture API korzysta z wzorca projektowego konstruktora. Aplikacje używają Recorder.Builder do tworzenia Recorder. Możesz też skonfigurować rozdzielczość filmu dla Recorder za pomocą obiektu QualitySelector.

CameraXRecorder obsługuje te predefiniowane Qualities rozdzielczości wideo:

  • Quality.UHD w przypadku rozmiaru filmu w rozdzielczości 4K Ultra HD (2160p);
  • Quality.FHD w przypadku rozmiaru wideo Full HD (1080p);
  • Quality.HD rozmiar filmu HD (720p);
  • Quality.SD w przypadku rozmiaru filmu SD (480p);

Pamiętaj, że CameraX może też wybierać inne rozdzielczości, jeśli aplikacja na to zezwoli.

Dokładny rozmiar filmu zależy od możliwości kamery i enkodera. Więcej informacji znajdziesz w dokumentacji CamcorderProfile.

Aplikacje mogą skonfigurować rozdzielczość, tworząc QualitySelector. Możesz utworzyć QualitySelector za pomocą jednej z tych metod:

  • Podaj kilka preferowanych rozdzielczości, używając fromOrderedList(), i uwzględnij strategię rezerwową, która będzie używana, jeśli żadna z preferowanych rozdzielczości nie jest obsługiwana.

    CameraX może wybrać najlepsze dopasowanie na podstawie możliwości wybranego aparatu. Więcej informacji znajdziesz w QualitySelectorFallbackStrategy specification. Na przykład poniższy kod żąda najwyższej obsługiwanej rozdzielczości nagrywania. Jeśli żadna z żądanych rozdzielczości nie jest obsługiwana, zezwala na wybór przez CameraX rozdzielczości najbliższej rozdzielczości Quality.SD:

    val qualitySelector = QualitySelector.fromOrderedList(
             listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD),
             FallbackStrategy.lowerQualityOrHigherThan(Quality.SD))
    
  • Najpierw sprawdź możliwości kamery i wybierz jedną z obsługiwanych rozdzielczości za pomocą QualitySelector::from():

    val cameraInfo = cameraProvider.availableCameraInfos.filter {
        Camera2CameraInfo
        .from(it)
        .getCameraCharacteristic(CameraCharacteristics.LENS\_FACING) == CameraMetadata.LENS_FACING_BACK
    }
    
    val supportedQualities = QualitySelector.getSupportedQualities(cameraInfo[0])
    val filteredQualities = arrayListOf (Quality.UHD, Quality.FHD, Quality.HD, Quality.SD)
                           .filter { supportedQualities.contains(it) }
    
    // Use a simple ListView with the id of simple_quality_list_view
    viewBinding.simpleQualityListView.apply {
        adapter = ArrayAdapter(context,
                               android.R.layout.simple_list_item_1,
                               filteredQualities.map { it.qualityToString() })
    
        // Set up the user interaction to manually show or hide the system UI.
        setOnItemClickListener { _, _, position, _ ->
            // Inside View.OnClickListener,
            // convert Quality.* constant to QualitySelector
            val qualitySelector = QualitySelector.from(filteredQualities[position])
    
            // Create a new Recorder/VideoCapture for the new quality
            // and bind to lifecycle
            val recorder = Recorder.Builder()
                .setQualitySelector(qualitySelector).build()
    
             // ...
        }
    }
    
    // A helper function to translate Quality to a string
    fun Quality.qualityToString() : String {
        return when (this) {
            Quality.UHD -> "UHD"
            Quality.FHD -> "FHD"
            Quality.HD -> "HD"
            Quality.SD -> "SD"
            else -> throw IllegalArgumentException()
        }
    }
    
    

    Pamiętaj, że zwrócona funkcja z QualitySelector.getSupportedQualities() na pewno będzie działać w przypadku VideoCapture lub kombinacji VideoCapturePreview. W przypadku łączenia z ImageCapture lub ImageAnalysis CameraX może nadal nie powiązać się, jeśli wymagana kombinacja nie jest obsługiwana przez żądany aparat.

Gdy masz już QualitySelector, aplikacja może utworzyć obiekt VideoCapture i przeprowadzić wiązanie. Pamiętaj, że to powiązanie jest takie samo jak w przypadku innych zastosowań:

val recorder = Recorder.Builder()
    .setExecutor(cameraExecutor).setQualitySelector(qualitySelector)
    .build()
val videoCapture = VideoCapture.withOutput(recorder)

try {
    // Bind use cases to camera
    cameraProvider.bindToLifecycle(
            this, CameraSelector.DEFAULT_BACK_CAMERA, preview, videoCapture)
} catch(exc: Exception) {
    Log.e(TAG, "Use case binding failed", exc)
}

Pamiętaj, że bindToLifecycle() zwraca obiekt Camera. Więcej informacji o sterowaniu danymi wyjściowymi kamery, takimi jak zoom i ekspozycja, znajdziesz w tym przewodniku.

Recorder wybiera format najbardziej odpowiedni dla systemu. Najpopularniejszym kodekiem wideo jest H.264 AVC, a formatem kontenera jest MPEG-4.

Konfigurowanie i tworzenie nagrania

Recorder aplikacja może tworzyć obiekty nagrywania, aby rejestrować obraz i dźwięk. Aplikacje tworzą nagrania w ten sposób:

  1. Skonfiguruj OutputOptions za pomocą prepareRecording().
  2. (Opcjonalnie) Włącz nagrywanie dźwięku.
  3. Użyj start(), aby zarejestrować VideoRecordEvent odbiorcę i rozpocząć nagrywanie wideo.

FunkcjaRecorder zwraca obiekt Recording, gdy wywołasz funkcję start(). Aplikacja może używać tego obiektu Recording do zakończenia przechwytywania lub wykonywania innych działań, takich jak wstrzymywanie i wznawianie.

Recorder obsługuje 1 obiekt Recording naraz. Nowe nagrywanie możesz rozpocząć po wywołaniu Recording.stop() lub Recording.close() na poprzednim obiekcie Recording.

Przyjrzyjmy się tym krokom bliżej. Najpierw aplikacja konfiguruje OutputOptions dla dyktafonu za pomocą Recorder.prepareRecording(). Recorder obsługuje te typy OutputOptions:

  • FileDescriptorOutputOptions do rejestrowania w FileDescriptor.
  • FileOutputOptions do rejestrowania w File.
  • MediaStoreOutputOptions do rejestrowania w MediaStore.

W przypadku wszystkich typów OutputOptions możesz ustawić maksymalny rozmiar pliku za pomocą parametru setFileSizeLimit(). Inne opcje są specyficzne dla poszczególnych typów danych wyjściowych, np. ParcelFileDescriptor w przypadku FileDescriptorOutputOptions.

prepareRecording() zwraca obiekt PendingRecording, który jest obiektem pośrednim używanym do tworzenia odpowiedniego obiektu Recording. PendingRecording to klasa przejściowa, która w większości przypadków powinna być niewidoczna i rzadko jest buforowana przez aplikację.

Aplikacje mogą dodatkowo konfigurować nagrywanie, np.:

  • Włącz dźwięk za pomocą urządzenia withAudioEnabled().
  • Zarejestruj detektor, aby otrzymywać zdarzenia nagrywania wideo za pomocą start(Executor, Consumer<VideoRecordEvent>).
  • Umożliwia ciągłe nagrywanie, gdy obiekt VideoCapture, do którego jest dołączony, zostanie ponownie powiązany z inną kamerą, z PendingRecording.asPersistentRecording().

Aby rozpocząć nagrywanie, zadzwoń pod numer PendingRecording.start(). CameraX przekształca PendingRecordingRecording, umieszcza żądanie nagrywania w kolejce i zwraca do aplikacji nowo utworzony obiekt Recording. Gdy nagrywanie na odpowiednim urządzeniu z kamerą się rozpocznie, CameraX wyśle zdarzenie VideoRecordEvent.EVENT_TYPE_START.

Poniższy przykład pokazuje, jak nagrać wideo i dźwięk do plikuMediaStore:

// Create MediaStoreOutputOptions for our recorder
val name = "CameraX-recording-" +
        SimpleDateFormat(FILENAME_FORMAT, Locale.US)
                .format(System.currentTimeMillis()) + ".mp4"
val contentValues = ContentValues().apply {
   put(MediaStore.Video.Media.DISPLAY_NAME, name)
}
val mediaStoreOutput = MediaStoreOutputOptions.Builder(this.contentResolver,
                              MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
                              .setContentValues(contentValues)
                              .build()

// 2. Configure Recorder and Start recording to the mediaStoreOutput.
val recording = videoCapture.output
                .prepareRecording(context, mediaStoreOutput)
                .withAudioEnabled()
                .start(ContextCompat.getMainExecutor(this), captureListener)

Podgląd z przedniego aparatu jest domyślnie odbity jak w lustrze, ale filmy nagrywane za pomocą VideoCapture nie są domyślnie odbite jak w lustrze. W CameraX 1.3 można teraz tworzyć odbicia lustrzane nagrań wideo, aby podgląd z przedniego aparatu i nagrany film były zgodne.

Dostępne są 3 opcje MirrorMode: MIRROR_MODE_OFF, MIRROR_MODE_ON i MIRROR_MODE_ON_FRONT_ONLY. Aby dopasować podgląd do aparatu, Google zaleca używanie trybu MIROR_MODE_ON_FRONT_ONLY, co oznacza, że odbicie lustrzane nie jest włączone w przypadku tylnego aparatu, ale jest włączone w przypadku przedniego aparatu. Więcej informacji o MirrorMode znajdziesz w sekcji MirrorMode constants.

Ten fragment kodu pokazuje, jak wywołać VideoCapture.Builder.setMirrorMode() za pomocą MIRROR_MODE_ON_FRONT_ONLY. Więcej informacji znajdziesz w sekcji setMirrorMode().

Kotlin

val recorder = Recorder.Builder().build()

val videoCapture = VideoCapture.Builder(recorder)
    .setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
    .build()

useCases.add(videoCapture);

Java

Recorder.Builder builder = new Recorder.Builder();
if (mVideoQuality != QUALITY_AUTO) {
    builder.setQualitySelector(
        QualitySelector.from(mVideoQuality));
}
  VideoCapture<Recorder> videoCapture = new VideoCapture.Builder<>(builder.build())
      .setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
      .build();
    useCases.add(videoCapture);

Sterowanie aktywnym nagraniem

Trwające Recording możesz wstrzymać, wznowić i zatrzymać, korzystając z tych metod:

  • pause aby wstrzymać bieżące aktywne nagrywanie.
  • resume() aby wznowić wstrzymane aktywne nagrywanie.
  • stop() aby zakończyć nagrywanie i usunąć wszystkie powiązane obiekty nagrywania.
  • mute() – wyciszanie lub wyłączanie wyciszenia bieżącego nagrania.

Pamiętaj, że możesz zadzwonić pod numer stop(), aby zakończyć Recording niezależnie od tego, czy nagrywanie jest wstrzymane, czy aktywne.

Jeśli masz zarejestrowany EventListenerPendingRecording.start(), Recording komunikuje się za pomocą VideoRecordEvent.

  • VideoRecordEvent.EVENT_TYPE_STATUS służy do rejestrowania statystyk, takich jak aktualny rozmiar pliku i czas nagrywania.
  • VideoRecordEvent.EVENT_TYPE_FINALIZE jest używany w przypadku wyniku nagrywania i zawiera informacje takie jak identyfikator URI pliku końcowego oraz wszelkie powiązane błędy.

Gdy aplikacja otrzyma EVENT_TYPE_FINALIZE wskazujący, że sesja nagrywania zakończyła się pomyślnie, możesz uzyskać dostęp do nagranego filmu z lokalizacji określonej w OutputOptions.

Dodatkowe materiały

Więcej informacji o bibliotece CameraX znajdziesz w tych materiałach: