同時使用多個相機串流畫面

注意:本頁面是指 Camera2 套件。除非應用程式需要 Camera2 的特定低階功能,否則建議使用 CameraX。CameraX 和 Camera2 均支援 Android 5.0 (API 級別 21) 以上版本。

相機應用程式可以同時使用多個影格。在某些情況下,不同的串流甚至需要不同的影格解析度或像素格式。常見的用途包括:

  • 錄影:一個用於預覽的串流,另一個正在編碼並儲存至檔案。
  • 條碼掃描:一個用於預覽的串流,另一個用於偵測條碼。
  • 計算攝影:一個用於預覽的串流,另一個用於偵測臉部/場景偵測。

處理影格時的效能成本可顯著,而且在執行平行串流或管道處理時,成本會乘以費用。

CPU、GPU 和 DSP 等資源或許可以利用架構的「重新處理」功能,但記憶體等資源將會線性成長。

每項要求多個目標

多個相機串流可合併為單一 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) 決定的最低 FPS 串流。實際效能因裝置而異,但 Android 根據以下三個變數,提供一定的支援特定組合:「輸出類型」、「輸出大小」和「硬體等級」

使用不支援的變數組合可能會以低影格速率運作;如果沒有,則會觸發其中一個失敗回呼。createCaptureSession 的說明文件說明瞭保證運作方式。

輸出類型

「輸出類型」是指影格的編碼格式。可能的值包括 PRIV、YUV、JPEG 和 RAW。如需相關說明,請參閱 createCaptureSession 的說明文件。

選擇應用程式的輸出類型時,如果您的目標是盡可能提高相容性,請使用 ImageFormat.YUV_420_888 進行影格分析,並使用 ImageFormat.JPEG 處理靜態圖片。針對預覽和錄製情境,您可能需要使用 SurfaceViewTextureViewMediaRecorderMediaCodecRenderScript.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() 列出,但只有兩種與相容性有關:PREVIEWMAXIMUM。尺寸會以上限表示。如果 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 (1920x1080) 的最佳尺寸,以較小者為準。顯示比例可能與螢幕的長寬比可能不符,因此您可能需要對串流套用上下黑邊或裁剪,才能以全螢幕模式顯示。如要取得正確的預覽大小,請比較可用的輸出大小與顯示大小,同時考慮螢幕可能會旋轉。

以下程式碼定義了 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);

將所有影片整合在一起

您可以使用輸出類型、輸出大小和硬體層級,判斷哪些串流組合是有效的。以下圖表為具有 LEGACY 硬體級別的 CameraDevice 支援的設定數據匯報。

目標 1 目標 2 目標 3 使用範例
類型 大小上限 類型 大小上限 類型 大小上限
PRIV MAXIMUM 簡易預覽、GPU 影片處理功能,或是不預覽錄影。
JPEG MAXIMUM 無觀景窗靜態影像拍攝。
YUV MAXIMUM 應用程式內的影片/圖片處理功能。
PRIV PREVIEW JPEG MAXIMUM 標準靜態影像。
YUV PREVIEW JPEG MAXIMUM 應用程式內處理和靜態拍攝功能。
PRIV PREVIEW PRIV PREVIEW 標準錄製。
PRIV PREVIEW YUV PREVIEW 提供預先發布版和應用程式內處理功能。
PRIV PREVIEW YUV PREVIEW 提供預先發布版和應用程式內處理功能。
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM 靜態畫面加上應用程式處理功能,

LEGACY 是可能的最低硬體等級。這個表格顯示,每部支援 Camera2 (API 級別 21 或以上版本) 的裝置都能以正確的設定輸出最多三個同時串流資料;如果沒有過多負載限制效能 (例如記憶體、CPU 或熱力限制),則最多可以輸出三個同時串流。

您的應用程式也必須設定指定輸出緩衝區。舉例來說,如要指定具有 LEGACY 硬體等級的裝置,您可以使用 ImageFormat.PRIVATE 設定兩個目標輸出介面,另一個則使用 ImageFormat.YUV_420_888。這是使用 PREVIEW 大小時支援的組合。如要使用本主題前面定義的函式,取得相機 ID 所需的預覽大小需要下列程式碼:

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

您可以呼叫 SurfaceHolder.setFixedSize() 來強制 SurfaceView 符合相機輸出內容大小,或採取與 GitHub 上相機範例常見模組類似的 AutoFitSurfaceView 方法,這樣就能設定絕對大小,同時將長寬比和可用空間納入考量,並會在觸發活動變更時自動進行調整。

沒有等待等待的回呼,因此從 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 硬體層級可指定最低的公分母裝置。在採用 LIMITED 硬體等級的裝置中,您可以為其中一個輸出目標介面新增條件式分支,並使用 RECORD 大小,甚至針對具備 FULL 硬體等級的裝置,將其增加到 MAXIMUM 大小。