Menggunakan beberapa streaming kamera secara bersamaan

Catatan: Halaman ini merujuk ke paket Camera2. Kecuali aplikasi Anda memerlukan fitur tingkat rendah khusus dari Camera2, sebaiknya gunakan CameraX. CameraX dan Camera2 mendukung Android 5.0 (level API 21) dan versi yang lebih baru.

Aplikasi kamera dapat menggunakan lebih dari satu aliran frame secara bersamaan. Pada beberapa kasus, stream yang berbeda bahkan memerlukan resolusi frame atau format piksel yang berbeda. Beberapa kasus penggunaan umum antara lain:

  • Perekaman video: satu streaming untuk pratinjau, streaming lainnya dienkode dan disimpan ke dalam file.
  • Pemindaian kode batang: satu streaming untuk pratinjau, satu streaming untuk deteksi kode batang.
  • Fotografi komputasi: satu streaming untuk pratinjau, satu lagi untuk deteksi wajah/adegan.

Ada biaya performa yang tidak umum saat memproses frame, dan biayanya berlipat ganda saat melakukan pemrosesan streaming atau pipeline paralel.

Resource seperti CPU, GPU, dan DSP mungkin dapat memanfaatkan kemampuan pemrosesan ulang framework, tetapi resource seperti memori akan bertambah secara linear.

Beberapa target per permintaan

Beberapa streaming kamera dapat digabungkan menjadi satu CameraCaptureRequest. Cuplikan kode berikut mengilustrasikan cara menyiapkan sesi kamera dengan satu streaming untuk pratinjau kamera dan streaming lainnya untuk pemrosesan gambar:

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

Jika Anda mengonfigurasi platform target dengan benar, kode ini hanya akan menghasilkan aliran yang memenuhi FPS minimum yang ditentukan oleh StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) dan StreamComfigurationMap.GetOutputStallDuration(int, Size). Performa sebenarnya bervariasi dari satu perangkat ke perangkat lainnya, meskipun Android memberikan beberapa jaminan untuk mendukung kombinasi tertentu, bergantung pada tiga variabel: jenis output, ukuran output, dan tingkat hardware.

Penggunaan kombinasi variabel yang tidak didukung dapat bekerja pada kecepatan frame rendah; jika tidak, fungsi ini akan memicu salah satu callback kegagalan. Dokumentasi untuk createCaptureSession menjelaskan apa saja yang dijamin akan berfungsi.

Jenis output

Jenis output mengacu pada format frame yang dienkode. Nilai yang memungkinkan adalah PRIV, YUV, JPEG, dan RAW. Dokumentasi untuk createCaptureSession menjelaskannya.

Saat memilih jenis output aplikasi, jika tujuannya adalah memaksimalkan kompatibilitas, gunakan ImageFormat.YUV_420_888 untuk analisis frame dan ImageFormat.JPEG untuk gambar diam. Untuk skenario pratinjau dan perekaman, Anda mungkin akan menggunakan SurfaceView, TextureView, MediaRecorder, MediaCodec, atau RenderScript.Allocation. Dalam kasus tersebut, jangan tentukan format gambar. Untuk kompatibilitas, ini akan dihitung sebagai ImageFormat.PRIVATE, terlepas dari format sebenarnya yang digunakan secara internal. Untuk membuat kueri format yang didukung oleh perangkat dengan CameraCharacteristics-nya, gunakan kode berikut:

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();

Ukuran output

Semua ukuran output yang tersedia dicantumkan oleh StreamConfigurationMap.getOutputSizes(), tetapi hanya dua yang terkait dengan kompatibilitas: PREVIEW dan MAXIMUM. Ukuran berfungsi sebagai batas atas. Jika sesuatu yang berukuran PREVIEW berfungsi, apa pun yang berukuran lebih kecil dari PREVIEW juga akan berfungsi. Hal yang sama berlaku untuk MAXIMUM. Dokumentasi untuk CameraDevice menjelaskan ukuran ini.

Ukuran output yang tersedia bergantung pada pilihan format. Dengan mempertimbangkan CameraCharacteristics dan formatnya, Anda dapat membuat kueri untuk ukuran output yang tersedia seperti ini:

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

Dalam kasus penggunaan perekaman dan pratinjau kamera, gunakan class target untuk menentukan ukuran yang didukung. Format akan ditangani oleh framework kamera itu sendiri:

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

Untuk mendapatkan ukuran MAXIMUM, urutkan ukuran output berdasarkan area dan tampilkan yang terbesar:

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 mengacu pada ukuran yang paling cocok dengan resolusi layar perangkat atau dengan 1080p (1920x1080), mana saja yang lebih kecil. Rasio aspek mungkin tidak sama persis dengan rasio aspek layar, sehingga Anda mungkin perlu menerapkan tampilan lebar atau pemangkasan pada streaming untuk menampilkannya dalam mode layar penuh. Untuk mendapatkan ukuran pratinjau yang tepat, bandingkan ukuran output yang tersedia dengan ukuran tampilan dengan mempertimbangkan bahwa layar mungkin diputar.

Kode berikut menentukan class helper, SmartSize, yang akan membuat perbandingan ukuran sedikit lebih mudah:

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

Memeriksa level hardware yang didukung

Untuk menentukan kemampuan yang tersedia saat runtime, periksa level hardware yang didukung menggunakan CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL.

Dengan objek CameraCharacteristics, Anda dapat mengambil level hardware dengan satu pernyataan:

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

Menggabungkan semua bagian

Dengan jenis output, ukuran output, dan tingkat hardware, Anda dapat menentukan kombinasi stream yang valid. Diagram berikut adalah snapshot konfigurasi yang didukung oleh CameraDevice dengan level hardware LEGACY.

Target 1 Target 2 Target 3 Contoh kasus penggunaan
Jenis Ukuran maksimal Jenis Ukuran maksimal Jenis Ukuran maksimal
PRIV MAXIMUM Pratinjau sederhana, pemrosesan video GPU, atau perekaman video tanpa pratinjau.
JPEG MAXIMUM Tidak ada pengambilan gambar diam jendela bidik.
YUV MAXIMUM Pemrosesan gambar/video dalam aplikasi.
PRIV PREVIEW JPEG MAXIMUM Gambar diam standar.
YUV PREVIEW JPEG MAXIMUM Pemrosesan dalam aplikasi plus gambar diam.
PRIV PREVIEW PRIV PREVIEW Perekaman standar.
PRIV PREVIEW YUV PREVIEW Pratinjau plus pemrosesan dalam aplikasi.
PRIV PREVIEW YUV PREVIEW Pratinjau plus pemrosesan dalam aplikasi.
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM Ambil gambar dan pemrosesan dalam aplikasi.

LEGACY adalah level hardware serendah mungkin. Tabel ini menunjukkan bahwa setiap perangkat yang mendukung Camera2 (API level 21 dan yang lebih tinggi) dapat menghasilkan hingga tiga aliran secara serentak menggunakan konfigurasi yang tepat, dan jika tidak ada terlalu banyak overhead yang membatasi performa, seperti batasan memori, CPU, atau termal.

Aplikasi Anda juga perlu mengonfigurasi buffering output penargetan. Misalnya, untuk menargetkan perangkat dengan level hardware LEGACY, Anda dapat menyiapkan dua platform output target, satu menggunakan ImageFormat.PRIVATE dan satu lagi menggunakan ImageFormat.YUV_420_888. Ini adalah kombinasi yang didukung saat menggunakan ukuran PREVIEW. Dengan menggunakan fungsi yang ditentukan sebelumnya dalam topik ini, mendapatkan ukuran pratinjau yang diperlukan untuk ID kamera memerlukan kode berikut:

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

Proses ini perlu menunggu hingga SurfaceView siap menggunakan callback yang disediakan:

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

Anda dapat memaksa SurfaceView agar sesuai dengan ukuran output kamera dengan memanggil SurfaceHolder.setFixedSize() atau Anda dapat melakukan pendekatan yang mirip dengan AutoFitSurfaceView dari Modul umum contoh kamera di GitHub, yang menetapkan ukuran absolut, dengan mempertimbangkan rasio aspek dan ruang yang tersedia, sambil otomatis menyesuaikan saat perubahan aktivitas dipicu.

Menyiapkan platform lain dari ImageReader dengan format yang diinginkan akan lebih mudah karena tidak ada callback yang harus ditunggu:

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

Saat menggunakan buffering target pemblokiran seperti ImageReader, hapus frame setelah menggunakannya:

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

Level hardware LEGACY menargetkan perangkat penyebut umum terendah. Anda dapat menambahkan pencabangan bersyarat dan menggunakan ukuran RECORD untuk salah satu platform target output di perangkat dengan level hardware LIMITED, atau bahkan meningkatkannya menjadi ukuran MAXIMUM untuk perangkat dengan level hardware FULL.