StreamUseCaseUtil.java

/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.camera.camera2.internal;

import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraMetadata;
import android.media.MediaCodec;
import android.os.Build;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.AttachedSurfaceInfo;
import androidx.camera.core.impl.CameraMode;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImageCaptureConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
import androidx.camera.core.streamsharing.StreamSharing;
import androidx.core.util.Preconditions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A class that contains utility methods for stream use case.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class StreamUseCaseUtil {

    public static final Config.Option<Long> STREAM_USE_CASE_STREAM_SPEC_OPTION =
            Config.Option.create("camera2.streamSpec.streamUseCase", long.class);

    private StreamUseCaseUtil() {

    }

    private static Map<Class<?>, Long> sUseCaseToStreamUseCaseMapping;

    /**
     * Populates the mapping between surfaces of a capture session and the Stream Use Case of their
     * associated stream.
     *
     * @param sessionConfigs   collection of all session configs for this capture session
     * @param streamUseCaseMap the mapping between surfaces and Stream Use Case flag
     */
    @OptIn(markerClass = ExperimentalCamera2Interop.class)
    public static void populateSurfaceToStreamUseCaseMapping(
            @NonNull Collection<SessionConfig> sessionConfigs,
            @NonNull Map<DeferrableSurface, Long> streamUseCaseMap,
            @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat,
            boolean shouldSetStreamUseCaseByDefault) {
        if (Build.VERSION.SDK_INT < 33) {
            return;
        }

        if (cameraCharacteristicsCompat.get(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES) == null) {
            return;
        }

        Set<Long> supportedStreamUseCases = new HashSet<>();
        for (long useCase : cameraCharacteristicsCompat.get(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES)) {
            supportedStreamUseCases.add(useCase);
        }

        for (SessionConfig sessionConfig : sessionConfigs) {
            if (sessionConfig.getTemplateType()
                    == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
            ) {
                // If is ZSL, do not populate anything.
                streamUseCaseMap.clear();
                return;
            }
            for (DeferrableSurface surface : sessionConfig.getSurfaces()) {
                if (sessionConfig.getImplementationOptions().containsOption(
                        Camera2ImplConfig.STREAM_USE_CASE_OPTION)
                        && putStreamUseCaseToMappingIfAvailable(
                        streamUseCaseMap,
                        surface,
                        sessionConfig.getImplementationOptions().retrieveOption(
                                Camera2ImplConfig.STREAM_USE_CASE_OPTION),
                        supportedStreamUseCases)) {
                    continue;
                }

                if (shouldSetStreamUseCaseByDefault) {
                    // TODO(b/266879290) This is currently gated out because of camera device
                    // crashing due to unsupported stream useCase combinations.
                    Long streamUseCase = getUseCaseToStreamUseCaseMapping()
                            .get(surface.getContainerClass());
                    putStreamUseCaseToMappingIfAvailable(streamUseCaseMap,
                            surface,
                            streamUseCase,
                            supportedStreamUseCases);
                }
            }
        }
    }

    private static boolean putStreamUseCaseToMappingIfAvailable(
            Map<DeferrableSurface, Long> streamUseCaseMap,
            DeferrableSurface surface,
            @Nullable Long streamUseCase,
            Set<Long> availableStreamUseCases) {
        if (streamUseCase == null) {
            return false;
        }

        if (!availableStreamUseCases.contains(streamUseCase)) {
            return false;
        }

        streamUseCaseMap.put(surface, streamUseCase);
        return true;
    }

    /**
     * Returns the mapping between the container class of a surface and the StreamUseCase
     * associated with that class. Refer to {@link UseCase} for the potential UseCase as the
     * container class for a given surface.
     */
    @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
    private static Map<Class<?>, Long> getUseCaseToStreamUseCaseMapping() {
        if (sUseCaseToStreamUseCaseMapping == null) {
            sUseCaseToStreamUseCaseMapping = new HashMap<>();
            sUseCaseToStreamUseCaseMapping.put(ImageAnalysis.class,
                    Long.valueOf(CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW));
            sUseCaseToStreamUseCaseMapping.put(Preview.class,
                    Long.valueOf(CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW));
            sUseCaseToStreamUseCaseMapping.put(ImageCapture.class,
                    Long.valueOf(CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE));
            sUseCaseToStreamUseCaseMapping.put(MediaCodec.class,
                    Long.valueOf(CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD));
            sUseCaseToStreamUseCaseMapping.put(StreamSharing.class,
                    Long.valueOf(CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD));
        }
        return sUseCaseToStreamUseCaseMapping;
    }

    /**
     * Populate all implementation options needed to determine the StreamUseCase option in the
     * StreamSpec for this UseCaseConfig
     */
    @OptIn(markerClass = ExperimentalCamera2Interop.class)
    @NonNull
    public static Camera2ImplConfig getStreamSpecImplementationOptions(
            @NonNull UseCaseConfig<?> useCaseConfig
    ) {
        MutableOptionsBundle optionsBundle = MutableOptionsBundle.create();
        if (useCaseConfig.containsOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION)) {
            optionsBundle.insertOption(
                    Camera2ImplConfig.STREAM_USE_CASE_OPTION,
                    useCaseConfig.retrieveOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION)
            );
        }
        if (useCaseConfig.containsOption(UseCaseConfig.OPTION_ZSL_DISABLED)) {
            optionsBundle.insertOption(
                    UseCaseConfig.OPTION_ZSL_DISABLED,
                    useCaseConfig.retrieveOption(UseCaseConfig.OPTION_ZSL_DISABLED)
            );
        }
        if (useCaseConfig.containsOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE)) {
            optionsBundle.insertOption(
                    ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE,
                    useCaseConfig
                            .retrieveOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE)
            );
        }
        if (useCaseConfig.containsOption(UseCaseConfig.OPTION_INPUT_FORMAT)) {
            optionsBundle.insertOption(
                    UseCaseConfig.OPTION_INPUT_FORMAT,
                    useCaseConfig
                            .retrieveOption(UseCaseConfig.OPTION_INPUT_FORMAT)
            );
        }
        return new Camera2ImplConfig(optionsBundle);
    }

    /**
     * Return true if the given camera characteristics support stream use case
     */
    @OptIn(markerClass = ExperimentalCamera2Interop.class)
    public static boolean isStreamUseCaseSupported(
            @NonNull CameraCharacteristicsCompat characteristicsCompat) {
        if (Build.VERSION.SDK_INT < 33) {
            return false;
        }
        long[] availableStreamUseCases = characteristicsCompat.get(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES);
        if (availableStreamUseCases == null || availableStreamUseCases.length == 0) {
            return false;
        }
        return true;
    }

    /**
     * Return true if the given feature settings is appropriate for stream use case usage.
     */
    public static boolean shouldUseStreamUseCase(@NonNull
            SupportedSurfaceCombination.FeatureSettings featureSettings) {
        return featureSettings.getCameraMode() == CameraMode.DEFAULT
                && featureSettings.getRequiredMaxBitDepth() == DynamicRange.BIT_DEPTH_8_BIT;
    }

    /**
     * Populate the {@link STREAM_USE_CASE_STREAM_SPEC_OPTION} option in StreamSpecs for both
     * existing UseCases and new UseCases to be attached. This option will be written into the
     * session configurations of the UseCases. When creating a new capture session during
     * downstream, it will be used to set the StreamUseCase flag via
     * {@link android.hardware.camera2.params.OutputConfiguration#setStreamUseCase(long)}
     *
     * @param characteristicsCompat        the camera characteristics of the device
     * @param attachedSurfaces             surface info of the already attached use cases
     * @param suggestedStreamSpecMap       the UseCaseConfig-to-StreamSpec map for new use cases
     * @param attachedSurfaceStreamSpecMap the SurfaceInfo-to-StreamSpec map for attached use cases
     *                                     whose StreamSpecs needs to be updated
     * @return true if StreamSpec options are populated. False if not.
     */
    @OptIn(markerClass = ExperimentalCamera2Interop.class)
    public static boolean populateStreamUseCaseStreamSpecOptionWithInteropOverride(
            @NonNull CameraCharacteristicsCompat characteristicsCompat,
            @NonNull List<AttachedSurfaceInfo> attachedSurfaces,
            @NonNull Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap,
            @NonNull Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap) {
        if (Build.VERSION.SDK_INT < 33) {
            return false;
        }
        List<UseCaseConfig<?>> newUseCaseConfigs = new ArrayList<>(suggestedStreamSpecMap.keySet());
        // All AttachedSurfaceInfo should have implementation options
        for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
            Preconditions.checkNotNull(attachedSurfaceInfo.getImplementationOptions());
        }
        // All StreamSpecs in the map should have implementation options
        for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
            Preconditions.checkNotNull(Preconditions.checkNotNull(
                    suggestedStreamSpecMap.get(useCaseConfig)).getImplementationOptions());
        }
        Set<Long> availableStreamUseCaseSet = new HashSet<>();
        long[] availableStreamUseCases = characteristicsCompat.get(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES);
        for (Long availableStreamUseCase : availableStreamUseCases) {
            availableStreamUseCaseSet.add(availableStreamUseCase);
        }
        if (isValidCamera2InteropOverride(attachedSurfaces, newUseCaseConfigs,
                availableStreamUseCaseSet)) {
            for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
                Config oldImplementationOptions = attachedSurfaceInfo.getImplementationOptions();
                Config newImplementationOptions =
                        getUpdatedImplementationOptionsWithUseCaseStreamSpecOption(
                                oldImplementationOptions,
                                oldImplementationOptions.retrieveOption(
                                        Camera2ImplConfig.STREAM_USE_CASE_OPTION));
                if (newImplementationOptions != null) {
                    StreamSpec.Builder newStreamSpecBuilder =
                            StreamSpec.builder(attachedSurfaceInfo.getSize())
                                    .setDynamicRange(attachedSurfaceInfo.getDynamicRange())
                                    .setImplementationOptions(newImplementationOptions);
                    if (attachedSurfaceInfo.getTargetFrameRate() != null) {
                        newStreamSpecBuilder.setExpectedFrameRateRange(
                                attachedSurfaceInfo.getTargetFrameRate());
                    }
                    attachedSurfaceStreamSpecMap.put(attachedSurfaceInfo,
                            newStreamSpecBuilder.build());
                }
            }
            for (UseCaseConfig<?> newUseCaseConfig : newUseCaseConfigs) {
                StreamSpec oldStreamSpec = suggestedStreamSpecMap.get(newUseCaseConfig);
                Config oldImplementationOptions = oldStreamSpec.getImplementationOptions();
                Config newImplementationOptions =
                        getUpdatedImplementationOptionsWithUseCaseStreamSpecOption(
                                oldImplementationOptions, oldImplementationOptions.retrieveOption(
                                        Camera2ImplConfig.STREAM_USE_CASE_OPTION));
                if (newImplementationOptions != null) {
                    StreamSpec newStreamSpec =
                            oldStreamSpec.toBuilder().setImplementationOptions(
                                    newImplementationOptions).build();
                    suggestedStreamSpecMap.put(newUseCaseConfig, newStreamSpec);
                }
            }
            return true;
        }
        return false;
    }

    /**
     * Given an old options, return a new option with stream use case stream spec option inserted
     */
    @Nullable
    @OptIn(markerClass = ExperimentalCamera2Interop.class)
    private static Config getUpdatedImplementationOptionsWithUseCaseStreamSpecOption(
            Config oldImplementationOptions,
            long streamUseCase
    ) {
        if (oldImplementationOptions.containsOption(STREAM_USE_CASE_STREAM_SPEC_OPTION)
                && oldImplementationOptions.retrieveOption(STREAM_USE_CASE_STREAM_SPEC_OPTION)
                == streamUseCase) {
            // The old options already has the same stream use case. No need to update
            return null;
        }
        MutableOptionsBundle optionsBundle =
                MutableOptionsBundle.from(oldImplementationOptions);
        optionsBundle.insertOption(STREAM_USE_CASE_STREAM_SPEC_OPTION, streamUseCase);
        return new Camera2ImplConfig(optionsBundle);
    }

    /**
     * Return true if any one of the existing or new UseCases is ZSL.
     */
    public static boolean containsZslUseCase(
            @NonNull List<AttachedSurfaceInfo> attachedSurfaces,
            @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
        for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
            List<UseCaseConfigFactory.CaptureType> captureTypes =
                    attachedSurfaceInfo.getCaptureTypes();
            UseCaseConfigFactory.CaptureType captureType = captureTypes.get(0);
            if (isZslUseCase(
                    attachedSurfaceInfo.getImplementationOptions(),
                    captureType)) {
                return true;
            }
        }
        for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
            if (isZslUseCase(useCaseConfig, useCaseConfig.getCaptureType())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check whether a UseCase is ZSL.
     */
    private static boolean isZslUseCase(Config config,
            UseCaseConfigFactory.CaptureType captureType) {
        if (config.retrieveOption(UseCaseConfig.OPTION_ZSL_DISABLED, false)) {
            return false;
        }
        // Skip if capture mode doesn't exist in the options
        if (!config.containsOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE)) {
            return false;
        }

        @ImageCapture.CaptureMode int captureMode =
                config.retrieveOption(ImageCaptureConfig.OPTION_IMAGE_CAPTURE_MODE);
        return TemplateTypeUtil.getSessionConfigTemplateType(captureType, captureMode)
                == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG;
    }

    /**
     * Check whether the given StreamUseCases are available to the device.
     */
    private static boolean areStreamUseCasesAvailable(Set<Long> availableStreamUseCasesSet,
            Set<Long> streamUseCases) {
        for (Long streamUseCase : streamUseCases) {
            if (!availableStreamUseCasesSet.contains(streamUseCase)) {
                return false;
            }
        }
        return true;
    }

    private static void throwInvalidCamera2InteropOverrideException() {
        throw new IllegalArgumentException("Either all use cases must have non-default stream use "
                + "case assigned or none should have it");
    }

    /**
     * Return true if all existing UseCases and new UseCases have Camera2Interop override and
     * these StreamUseCases are all available to the device.
     */
    @OptIn(markerClass = ExperimentalCamera2Interop.class)
    private static boolean isValidCamera2InteropOverride(
            List<AttachedSurfaceInfo> attachedSurfaces,
            List<UseCaseConfig<?>> newUseCaseConfigs,
            Set<Long> availableStreamUseCases) {
        Set<Long> streamUseCases = new HashSet<>();
        boolean hasNonDefaultStreamUseCase = false;
        boolean hasDefaultOrNullStreamUseCase = false;
        for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
            if (!attachedSurfaceInfo.getImplementationOptions().containsOption(
                    Camera2ImplConfig.STREAM_USE_CASE_OPTION)) {
                hasDefaultOrNullStreamUseCase = true;
                break;
            }
            long streamUseCaseOverride =
                    attachedSurfaceInfo.getImplementationOptions()
                            .retrieveOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION);
            if (streamUseCaseOverride
                    == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT) {
                hasDefaultOrNullStreamUseCase = true;
                break;
            }
            hasNonDefaultStreamUseCase = true;
            break;
        }
        for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
            if (!useCaseConfig.containsOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION)) {
                hasDefaultOrNullStreamUseCase = true;
                if (hasNonDefaultStreamUseCase) {
                    throwInvalidCamera2InteropOverrideException();
                }
            } else {
                long streamUseCaseOverride =
                        useCaseConfig.retrieveOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION);
                if (streamUseCaseOverride
                        == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT) {
                    hasDefaultOrNullStreamUseCase = true;
                    if (hasNonDefaultStreamUseCase) {
                        throwInvalidCamera2InteropOverrideException();
                    }
                } else {
                    hasNonDefaultStreamUseCase = true;
                    if (hasDefaultOrNullStreamUseCase) {
                        throwInvalidCamera2InteropOverrideException();
                    }
                    streamUseCases.add(streamUseCaseOverride);
                }
            }

        }
        return !hasDefaultOrNullStreamUseCase && areStreamUseCasesAvailable(availableStreamUseCases,
                streamUseCases);
    }
}