Arquitetura de captura de vídeo do CameraX

Um sistema de captura geralmente grava streams de vídeo e áudio, faz a compactação e a multiplexação deles e grava o stream resultante no disco.

Diagrama conceitual para um sistema de captura de vídeo e áudio
Figura 1. Diagrama conceitual para um sistema de captura de vídeo e áudio.

No CameraX, a solução para captura de vídeo é o caso de uso de VideoCapture:

diagrama conceitual que mostra como o CameraX lida com o
         caso de uso de captura de vídeo
Figura 2. Diagrama conceitual que mostra como o CameraX lida com o caso de uso VideoCapture.

Como mostrado na figura 2, a captura de vídeo do CameraX inclui alguns componentes de arquitetura de alto nível:

  • SurfaceProvider para a origem do vídeo.
  • AudioSource para a fonte de áudio.
  • Dois codificadores para codificar e compactar vídeo/áudio.
  • Um multiplexador de mídia para combinar os dois streams.
  • Um protetor de arquivo para gravar o resultado.

A API VideoCapture abstrai o mecanismo de captura complexo e fornece aplicativos com uma API muito mais simples e direta.

Visão geral da API VideoCapture

A VideoCapture é um caso de uso do CameraX que funciona bem sozinho ou quando combinado com outros casos de uso. O suporte a combinações específicas depende dos recursos de hardware da câmera, mas Preview e VideoCapture são uma combinação de casos de uso válida em todos os dispositivos.

A API VideoCapture consiste nos objetos abaixo que se comunicam com aplicativos:

  • A VideoCapture é a classe do caso de uso de nível mais alto. Ela se vincula a uma interface LifecycleOwner com uma classe CameraSelector e outros UseCases da CameraX. Para ver mais informações sobre esses conceitos e usos, consulte a Arquitetura da CameraX.
  • Um objeto Recorder é uma implementação de VideoOutput que está rigidamente acoplada à VideoCapture. O Recorder é usado para executar a captura de vídeo e de áudio. Um aplicativo cria gravações usando um Recorder.
  • Uma PendingRecording configura uma gravação, fornecendo opções como ativação de áudio e definição de um listener de eventos. É necessário usar um Recorder para criar uma PendingRecording. A PendingRecording não grava nada.
  • Uma Recording realiza a gravação em si. É necessário usar uma PendingRecording para criar uma Recording.

A figura 3 mostra as relações entre esses objetos:

diagrama mostrando as interações que ocorrem em um caso de uso de
         captura de vídeo
Figura 3. Diagrama mostrando as interações que ocorrem em um caso de uso de VideoCapture.

Legenda:

  1. Crie um objeto Recorder com o QualitySelector.
  2. Configure o Recorder com uma das OutputOptions.
  3. Ative o áudio com o método withAudioEnabled(), se necessário.
  4. Chame start() com um listener VideoRecordEvent para começar a gravar.
  5. Use pause()/resume()/stop() na Recording para controlar a gravação.
  6. Responda a VideoRecordEvents no listener de eventos.

A lista detalhada da API está no current.txt dentro do código-fonte (link em inglês).

Como usar a API VideoCapture

Para integrar o caso de uso da VideoCapture do CameraX ao seu app, siga estas etapas:

  1. Vincule a VideoCapture.
  2. Prepare e configure a gravação.
  3. Inicie e controle a gravação em execução.

Veja nas seções abaixo o que você pode fazer em cada etapa para ter uma sessão de gravação completa.

Vincular a VideoCapture

Para vincular o caso de uso da VideoCapure, siga estas etapas:

  1. Crie um objeto Recorder.
  2. Crie um objeto VideoCapture.
  3. Vincule um Lifecycle.

A API VideoCapture do CameraX segue o padrão de design do builder. Os aplicativos usam o Recorder.Builder para criar um Recorder. Você também pode configurar a resolução de vídeo para o Recorder usando um objeto QualitySelector.

O Recorder da CameraX oferece suporte a estas Qualities predefinidas para resoluções de vídeo:

  • Quality.UHD para vídeo em 4K Ultra HD (2160p)
  • Quality.FHD para vídeo em Full HD (1080p)
  • Quality.HD para vídeo em HD (720p)
  • Quality.SD para vídeo em SD (480p)

O CameraX também pode escolher outras resoluções quando permitido pelo app.

O tamanho exato do vídeo de cada seleção depende dos recursos da câmera e do codificador. Para mais informações, consulte a documentação do CamcorderProfile.

Os aplicativos podem criar um QualitySelector para configurar a resolução. Você pode criar um QualitySelector usando um destes métodos:

  • Use fromOrderedList() para fornecer algumas resoluções preferenciais e inclua uma estratégia substituta para ser usada caso não haja suporte a nenhuma delas.

    A CameraX pode decidir a melhor correspondência substituta com base na capacidade da câmera selecionada. Consulte a FallbackStrategy specification do QualitySelector para ver mais detalhes. Por exemplo, o código abaixo solicita a resolução mais alta com suporte à gravação e, se nenhuma das resoluções da solicitação tiver suporte, autoriza a CameraX a escolher uma que seja a mais próxima da qualidade Quality.SD:

    val qualitySelector = QualitySelector.fromOrderedList(
             listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD),
             FallbackStrategy.lowerQualityOrHigherThan(Quality.SD))
    
  • Primeiro, consulte os recursos da câmera e escolha uma das resoluções com suporte usando o método 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()
        }
    }
    
    

    O recurso retornado de QualitySelector.getSupportedQualities() tem garantia de funcionar para o caso de uso VideoCapture ou para a combinação de casos de uso VideoCapture e Preview. A vinculação na CameraX com os casos de uso ImageCapture ou ImageAnalysis ainda pode falhar quando a combinação necessária não tiver suporte à câmera solicitada.

Quando você tiver um QualitySelector, o aplicativo poderá criar um objeto VideoCapture e realizar a vinculação. Observe que essa vinculação é igual a outros casos de uso:

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)
}

Observe que bindToLifecycle() retorna um objeto Camera. Consulte este guia para ver mais informações sobre como controlar a saída da câmera, por exemplo, zoom e exposição.

O Recorder seleciona o formato mais adequado para o sistema. O codec de vídeo mais comum é o H.264 AVC, com formato de contêiner MPEG-4.

Configurar e criar uma gravação

Usando um Recorder, o aplicativo pode criar objetos de gravação para realizar a captura de vídeo e áudio. Para criar gravações em aplicativos siga estas etapas:

  1. Configure a classe OutputOptions usando o método prepareRecording().
  2. (Opcional) Ative a gravação de áudio.
  3. Use a função start() para registrar um listener VideoRecordEvent e inicie a captura de vídeo.

O Recorder retorna um objeto Recording quando você chama a função start(). O aplicativo pode usar esse objeto Recording para concluir a captura ou realizar outras ações, como pausar ou retomar.

Um Recorder oferece suporte a um objeto Recording por vez. Você pode iniciar uma nova gravação depois de chamar o método Recording.stop() ou Recording.close() no objeto Recording anterior.

Vamos analisar as etapas com mais detalhes. Primeiro, o aplicativo configura as OutputOptions para um Gravador usando o método Recorder.prepareRecording(). Um Recorder oferece suporte aos tipos de OutputOptions abaixo:

  • FileDescriptorOutputOptions para capturar um FileDescriptor.
  • FileOutputOptions para capturar um File.
  • MediaStoreOutputOptions para capturar um MediaStore.

Todos os tipos de OutputOptions permitem definir um tamanho máximo de arquivo usando o método setFileSizeLimit(). Outras opções são específicas do tipo de saída individual, como o ParcelFileDescriptor para FileDescriptorOutputOptions.

O método prepareRecording() retorna um objeto PendingRecording, que é um objeto intermediário usado para criar o objeto Recording correspondente. A PendingRecording é uma classe temporária que precisa ficar invisível na maioria dos casos e raramente é armazenada em cache pelo app.

Os aplicativos podem fazer mais configurações para a gravação, como:

  • ativar o áudio usando o método withAudioEnabled();
  • registrar um listener para receber eventos de gravação de vídeo usando start(Executor, Consumer<VideoRecordEvent>).
  • Permitir que uma gravação seja gravada continuamente enquanto a VideoCapture a que está anexada é novamente vinculada a outra câmera, com PendingRecording.asPersistentRecording().

Para iniciar a gravação, chame PendingRecording.start(). A CameraX transforma a PendingRecording em uma Recording, coloca a solicitação de gravação em fila e retorna o objeto Recording recém-criado ao aplicativo. Quando a gravação é iniciada no dispositivo de câmera correspondente, a CameraX envia um evento VideoRecordEvent.EVENT_TYPE_START.

O exemplo abaixo mostra como gravar vídeos e áudio em um arquivo MediaStore:

// 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)

Embora a visualização da câmera seja espelhada na câmera frontal por padrão, os vídeos gravados pela VideoCapture não serão espelhados por padrão. Com o CameraX 1.3, agora é possível espelhar gravações de vídeo para que a visualização da câmera frontal e o vídeo gravado correspondam.

Há três opções de MirrorMode: MIRROR_MODE_OFF, MIRROR_MODE_ON e MIRROR_MODE_ON_FRONT_Only. Para se alinhar à visualização da câmera, o Google recomenda o uso de MIROR_MODE_ON_FRONT_Only, o que significa que o espelhamento não está ativado para a câmera traseira, mas está ativado para a frente. Para mais informações sobre o MirrorMode, consulte MirrorMode constants.

O snippet de código abaixo mostra como chamar VideoCapture.Builder.setMirrorMode() usando MIRROR_MODE_ON_FRONT_ONLY. Para mais informações, consulte 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);

Controlar uma gravação ativa

Você pode pausar, retomar e parar uma Recording em andamento usando estes métodos:

  • pause para pausar a gravação ativa atual.
  • resume() para retomar uma gravação ativa pausada.
  • stop() para concluir a gravação e transferir todos os objetos de gravação associados.
  • mute() para ativar ou desativar o som da gravação atual.

É possível chamar o método stop() para encerrar uma Recording, independente de a gravação estar ou não pausada.

Se você registrou um EventListener com PendingRecording.start(), a Recording vai se comunicar usando um listener VideoRecordEvent.

  • O VideoRecordEvent.EVENT_TYPE_STATUS é usado para gravar estatísticas como o tamanho de arquivo atual e o período gravado.
  • O VideoRecordEvent.EVENT_TYPE_FINALIZE é usado para o resultado da gravação e inclui informações como o URI do arquivo final, além de qualquer erro relacionado.

Depois que o app receber um EVENT_TYPE_FINALIZE, que indica uma sessão de gravação bem-sucedida, você poderá acessar o vídeo capturado no local especificado nas OutputOptions.

Outros recursos

Para saber mais sobre o CameraX, consulte os recursos abaixo: