Как использовать несколько трансляций с камеры одновременно

Примечание. На этой странице рассматривается пакет Camera2. Если вашему приложению не требуются определенные низкоуровневые функции Camera2, рекомендуем использовать CameraX. CameraX и Camera2 поддерживают Android 5.0 (уровень API 21) и более поздние версии.

Приложение камеры может использовать несколько потоков кадров одновременно. В некоторых случаях для разных потоков даже требуется разное разрешение кадра или формат пикселей. Вот несколько примеров:

  • Запись видео. Один поток используется для предварительного просмотра, а другой кодируется и сохраняется в файл.
  • Сканирование штрихкодов. Один поток для предварительного просмотра, другой – для распознавания штрихкодов.
  • Вычислительная фотография. Один поток для предварительного просмотра, другой – для распознавания лиц и сцен.

Обработка кадров требует значительных ресурсов, а при параллельной обработке потоков или конвейеров затраты возрастают.

Такие ресурсы, как ЦП, графический процессор и цифровой сигнальный процессор, могут использовать возможности повторной обработки фреймворка, но такие ресурсы, как память, будут расти линейно.

Несколько целей на запрос

Несколько видео с камер можно объединить в один CameraCaptureRequest. В следующем фрагменте кода показано, как настроить сеанс камеры с одним потоком для предварительного просмотра и другим – для обработки изображений.

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback

// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD
val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
val combinedRequest = session.device.createCaptureRequest(requestTemplate)

// Link the Surface targets with the combined request
combinedRequest.addTarget(previewSurface)
combinedRequest.addTarget(imReaderSurface)

// In this simple case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request
session.setRepeatingRequest(combinedRequest.build(), null, null)

Java

CameraCaptureSession session = …;  // from CameraCaptureSession.StateCallback

// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use TEMPLATE_RECORD
        CaptureRequest.Builder combinedRequest = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

// Link the Surface targets with the combined request
        combinedRequest.addTarget(previewSurface);
        combinedRequest.addTarget(imReaderSurface);

// In this simple case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request
        session.setRepeatingRequest(combinedRequest.build(), null, null);

Если вы правильно настроите целевые поверхности, этот код будет создавать только потоки, соответствующие минимальной частоте кадров, определяемой StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) и StreamComfigurationMap.GetOutputStallDuration(int, Size). Фактическая производительность зависит от устройства, но Android гарантирует поддержку определенных комбинаций в зависимости от трех переменных: типа вывода, размера вывода и уровня оборудования.

Если вы используете неподдерживаемую комбинацию переменных, то частота кадров может быть низкой. Если это не так, то будет активирован один из обратных вызовов при сбое. В документации по createCaptureSession описано, работа каких функций гарантируется.

Тип документа

Тип вывода – это формат, в котором закодированы кадры. Возможные значения: PRIV, YUV, JPEG и RAW. Они описаны в документации по createCaptureSession.

Если вы хотите обеспечить максимальную совместимость, используйте ImageFormat.YUV_420_888 для анализа кадров и ImageFormat.JPEG для неподвижных изображений. Для предварительного просмотра и записи, скорее всего, будут использоваться следующие классы: SurfaceView, TextureView, MediaRecorder, MediaCodec или RenderScript.Allocation. В таких случаях не указывайте формат изображения. Для совместимости он будет считаться как ImageFormat.PRIVATE независимо от того, какой формат используется на самом деле. Чтобы запросить форматы, поддерживаемые устройством с определенным значением CameraCharacteristics, используйте следующий код:

Kotlin

val characteristics: CameraCharacteristics = ...
val supportedFormats = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).outputFormats

Java

CameraCharacteristics characteristics = …;
        int[] supportedFormats = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputFormats();

Размер выходных данных

Все доступные размеры выходных файлов перечислены в StreamConfigurationMap.getOutputSizes(), но только два из них связаны с совместимостью: PREVIEW и MAXIMUM. Размеры действуют как верхние границы. Если подходит размер PREVIEW, то подойдет и любой размер меньше PREVIEW. То же самое относится к MAXIMUM. Подробные сведения о размерах приведены в документации по CameraDevice.

Доступные размеры выходных файлов зависят от выбранного формата. Если у вас есть CameraCharacteristics и формат, вы можете запросить доступные размеры выходных данных следующим образом:

Kotlin

val characteristics: CameraCharacteristics = ...
val outputFormat: Int = ...  // such as ImageFormat.JPEG
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    .getOutputSizes(outputFormat)

Java

CameraCharacteristics characteristics = …;
        int outputFormat = …;  // such as ImageFormat.JPEG
Size[] sizes = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(outputFormat);

В случаях предварительного просмотра и записи с камеры используйте целевой класс, чтобы определить поддерживаемые размеры. Формат будет обрабатываться самим фреймворком камеры:

Kotlin

val characteristics: CameraCharacteristics = ...
val targetClass: Class <T> = ...  // such as SurfaceView::class.java
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    .getOutputSizes(targetClass)

Java

CameraCharacteristics characteristics = …;
   int outputFormat = …;  // such as ImageFormat.JPEG
   Size[] sizes = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                .getOutputSizes(outputFormat);

Чтобы получить размер MAXIMUM, отсортируйте размеры по площади и выберите наибольший:

Kotlin

fun <T>getMaximumOutputSize(
    characteristics: CameraCharacteristics, targetClass: Class <T>, format: Int? = null):
    Size {
  val config = characteristics.get(
      CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)

  // If image format is provided, use it to determine supported sizes; or else use target class
  val allSizes = if (format == null)
    config.getOutputSizes(targetClass) else config.getOutputSizes(format)
  return allSizes.maxBy { it.height * it.width }
}

Java

 @RequiresApi(api = Build.VERSION_CODES.N)
    <T> Size getMaximumOutputSize(CameraCharacteristics characteristics,
                                            Class <T> targetClass,
                                            Integer format) {
        StreamConfigurationMap config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

        // If image format is provided, use it to determine supported sizes; else use target class
        Size[] allSizes;
        if (format == null) {
            allSizes = config.getOutputSizes(targetClass);
        } else {
            allSizes = config.getOutputSizes(format);
        }
        return Arrays.stream(allSizes).max(Comparator.comparing(s -> s.getHeight() * s.getWidth())).get();
    }

PREVIEW — это разрешение, которое лучше всего подходит для разрешения экрана устройства, или 1080p (1920 x 1080) в зависимости от того, что меньше. Соотношение сторон может не совпадать с соотношением сторон экрана, поэтому вам может потребоваться применить кашетирование или обрезку к потоку, чтобы отобразить его в полноэкранном режиме. Чтобы получить правильный размер предварительного просмотра, сравните доступные размеры вывода с размером дисплея, учитывая, что дисплей может быть повернут.

В приведенном ниже коде определяется вспомогательный класс SmartSize, который упрощает сравнение размеров.

Kotlin

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
class SmartSize(width: Int, height: Int) {
    var size = Size(width, height)
    var long = max(size.width, size.height)
    var short = min(size.width, size.height)
    override fun toString() = "SmartSize(${long}x${short})"
}

/** Standard High Definition size for pictures and video */
val SIZE_1080P: SmartSize = SmartSize(1920, 1080)

/** Returns a [SmartSize] object for the given [Display] */
fun getDisplaySmartSize(display: Display): SmartSize {
    val outPoint = Point()
    display.getRealSize(outPoint)
    return SmartSize(outPoint.x, outPoint.y)
}

/**
 * Returns the largest available PREVIEW size. For more information, see:
 * https://d.android.com/reference/android/hardware/camera2/CameraDevice
 */
fun <T>getPreviewOutputSize(
        display: Display,
        characteristics: CameraCharacteristics,
        targetClass: Class <T>,
        format: Int? = null
): Size {

    // Find which is smaller: screen or 1080p
    val screenSize = getDisplaySmartSize(display)
    val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short
    val maxSize = if (hdScreen) SIZE_1080P else screenSize

    // If image format is provided, use it to determine supported sizes; else use target class
    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    if (format == null)
        assert(StreamConfigurationMap.isOutputSupportedFor(targetClass))
    else
        assert(config.isOutputSupportedFor(format))
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)

    // Get available sizes and sort them by area from largest to smallest
    val validSizes = allSizes
            .sortedWith(compareBy { it.height * it.width })
            .map { SmartSize(it.width, it.height) }.reversed()

    // Then, get the largest output size that is smaller or equal than our max size
    return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size
}

Java

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
    class SmartSize {
        Size size;
        double longSize;
        double shortSize;

        public SmartSize(Integer width, Integer height) {
            size = new Size(width, height);
            longSize = max(size.getWidth(), size.getHeight());
            shortSize = min(size.getWidth(), size.getHeight());
        }

        @Override
        public String toString() {
            return String.format("SmartSize(%sx%s)", longSize, shortSize);
        }
    }

    /** Standard High Definition size for pictures and video */
    SmartSize SIZE_1080P = new SmartSize(1920, 1080);

    /** Returns a [SmartSize] object for the given [Display] */
    SmartSize getDisplaySmartSize(Display display) {
        Point outPoint = new Point();
        display.getRealSize(outPoint);
        return new SmartSize(outPoint.x, outPoint.y);
    }

    /**
     * Returns the largest available PREVIEW size. For more information, see:
     * https://d.android.com/reference/android/hardware/camera2/CameraDevice
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    <T> Size getPreviewOutputSize(
            Display display,
            CameraCharacteristics characteristics,
            Class <T> targetClass,
            Integer format
    ){

        // Find which is smaller: screen or 1080p
        SmartSize screenSize = getDisplaySmartSize(display);
        boolean hdScreen = screenSize.longSize >= SIZE_1080P.longSize || screenSize.shortSize >= SIZE_1080P.shortSize;
        SmartSize maxSize;
        if (hdScreen) {
            maxSize = SIZE_1080P;
        } else {
            maxSize = screenSize;
        }

        // If image format is provided, use it to determine supported sizes; else use target class
        StreamConfigurationMap config = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (format == null)
            assert(StreamConfigurationMap.isOutputSupportedFor(targetClass));
        else
            assert(config.isOutputSupportedFor(format));
        Size[] allSizes;
        if (format == null) {
            allSizes = config.getOutputSizes(targetClass);
        } else {
            allSizes = config.getOutputSizes(format);
        }

        // Get available sizes and sort them by area from largest to smallest
        List <Size> sortedSizes = Arrays.asList(allSizes);
        List <SmartSize> validSizes =
                sortedSizes.stream()
                        .sorted(Comparator.comparing(s -> s.getHeight() * s.getWidth()))
                        .map(s -> new SmartSize(s.getWidth(), s.getHeight()))
                        .sorted(Collections.reverseOrder()).collect(Collectors.toList());

        // Then, get the largest output size that is smaller or equal than our max size
        return validSizes.stream()
                .filter(s -> s.longSize <= maxSize.longSize && s.shortSize <= maxSize.shortSize)
                .findFirst().get().size;
    }

Проверьте уровень поддержки аппаратного обеспечения

Чтобы определить доступные возможности во время выполнения, проверьте поддерживаемый уровень оборудования с помощью CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL.

С помощью объекта CameraCharacteristics можно получить уровень аппаратного обеспечения, используя одну инструкцию:

Kotlin

val characteristics: CameraCharacteristics = ...

// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
val hardwareLevel = characteristics.get(
        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)

Java

CameraCharacteristics characteristics = ...;

// Hardware level will be one of:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
Integer hardwareLevel = characteristics.get(
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);

Объединение всех частей

Тип и размер выходных данных, а также уровень аппаратного обеспечения позволяют определить, какие комбинации потоков допустимы. На диаграмме ниже показаны конфигурации, поддерживаемые CameraDevice с уровнем аппаратного обеспечения LEGACY.

Цель 1 Цель 2 Цель 3 Примеры использования
Тип Максимальный размер Тип Максимальный размер Тип Максимальный размер
PRIV MAXIMUM Простой предварительный просмотр, обработка видео графическим процессором или запись видео без предварительного просмотра.
JPEG MAXIMUM Фотосъемка без видоискателя.
YUV MAXIMUM Обработка видео и изображений в приложении.
PRIV PREVIEW JPEG MAXIMUM Стандартная фотосъемка.
YUV PREVIEW JPEG MAXIMUM Обработка в приложении и съемка фотографий.
PRIV PREVIEW PRIV PREVIEW Стандартная запись.
PRIV PREVIEW YUV PREVIEW Предварительная версия и обработка в приложении.
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM Фотосъемка и обработка в приложении.

LEGACY – самый низкий уровень аппаратного обеспечения. Из таблицы видно, что каждое устройство, поддерживающее Camera2 (уровень API 21 и выше), может выводить до трех одновременных потоков при правильной конфигурации и отсутствии слишком больших накладных расходов, ограничивающих производительность, таких как ограничения памяти, ЦП или температуры.

В приложении также необходимо настроить целевые выходные буферы. Например, чтобы настроить таргетинг на устройство с аппаратным уровнем LEGACY, можно задать две целевые области вывода: одну с использованием ImageFormat.PRIVATE, а другую – с использованием ImageFormat.YUV_420_888. Это поддерживаемое сочетание при использовании размера PREVIEW. Чтобы получить нужные размеры предварительного просмотра для идентификатора камеры, используя функцию, определенную ранее в этой теме, требуется следующий код:

Kotlin

val characteristics: CameraCharacteristics = ...
val context = this as Context  // assuming you are inside of an activity

val surfaceViewSize = getPreviewOutputSize(
    context, characteristics, SurfaceView::class.java)
val imageReaderSize = getPreviewOutputSize(
    context, characteristics, ImageReader::class.java, format = ImageFormat.YUV_420_888)

Java

CameraCharacteristics characteristics = ...;
        Context context = this; // assuming you are inside of an activity

        Size surfaceViewSize = getPreviewOutputSize(
                context, characteristics, SurfaceView.class);
        Size imageReaderSize = getPreviewOutputSize(
                context, characteristics, ImageReader.class, format = ImageFormat.YUV_420_888);

Для этого нужно дождаться готовности SurfaceView, используя предоставленные функции обратного вызова:

Kotlin

val surfaceView = findViewById <SurfaceView>(...)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
  override fun surfaceCreated(holder: SurfaceHolder) {
    // You do not need to specify image format, and it will be considered of type PRIV
    // Surface is now ready and you could use it as an output target for CameraSession
  }
  ...
})

Java

SurfaceView surfaceView = findViewById <SurfaceView>(...);

surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
                // You do not need to specify image format, and it will be considered of type PRIV
                // Surface is now ready and you could use it as an output target for CameraSession
            }
            ...
        });

Вы можете принудительно задать для SurfaceView размер, соответствующий размеру выходного изображения камеры, вызвав метод SurfaceHolder.setFixedSize(). Также можно использовать подход, аналогичный AutoFitSurfaceView из общего модуля примеров камеры на GitHub. Он задает абсолютный размер с учетом соотношения сторон и доступного пространства, а также автоматически корректирует его при изменении активности.

Настроить другую платформу из ImageReader с нужным форматом проще, поскольку не нужно ждать обратных вызовов.

Kotlin

val frameBufferCount = 3  // just an example, depends on your usage of ImageReader
val imageReader = ImageReader.newInstance(
    imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
    frameBufferCount)

Java

int frameBufferCount = 3;  // just an example, depends on your usage of ImageReader
ImageReader imageReader = ImageReader.newInstance(
                imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
                frameBufferCount);

Если вы используете блокирующий целевой буфер, например ImageReader, удаляйте кадры после их использования:

Kotlin

imageReader.setOnImageAvailableListener({
  val frame =  it.acquireNextImage()
  // Do something with "frame" here
  it.close()
}, null)

Java

imageReader.setOnImageAvailableListener(listener -> {
            Image frame = listener.acquireNextImage();
            // Do something with "frame" here
            listener.close();
        }, null);

Уровень оборудования LEGACY предназначен для устройств с наименьшим общим знаменателем. Вы можете добавить условное ветвление и использовать размер RECORD для одной из целевых поверхностей вывода на устройствах с уровнем аппаратной поддержки LIMITED или даже увеличить его до MAXIMUM для устройств с уровнем аппаратной поддержки FULL.