Skip to content
Snippets Groups Projects
Select Git revision
  • f927397579a212a6c32cda2a4e166ebcdc1a4111
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • release/201811
  • release/201808
  • wip/patches_poly_2017/cedryk_doucet/abderahmane_bouziane
  • releases/beta1
  • android/release_463
  • android/release_462
  • android/release_461
  • android/release_460
  • android/release_459
  • android/release_458
  • android/release_457
  • android/release_456
  • android/release_455
  • android/release_454
  • android/release_453
  • android/release_452
  • android/release_451
  • android/release_450
  • android/release_449
  • android/release_448
  • android/release_447
  • android/release_446
  • android/release_445
  • android/release_444
38 results

CameraService.java

Blame
  • Adrien Béraud's avatar
    Adrien Béraud authored
    Change-Id: I6e0690d4f590474c2c0ebf9c7c414c04c2ec5471
    f9273975
    History
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    CameraService.java 40.02 KiB
    /*
     *  Copyright (C) 2018-2019 Savoir-faire Linux Inc.
     *
     *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
     *
     *  This program is free software; you can redistribute it and/or modify
     *  it under the terms of the GNU General Public License as published by
     *  the Free Software Foundation; either version 3 of the License, or
     *  (at your option) any later version.
     *
     *  This program is distributed in the hope that it will be useful,
     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     *  GNU General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program; if not, write to the Free Software
     *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     */
    package cx.ring.services;
    
    import android.content.Context;
    import android.graphics.ImageFormat;
    import android.graphics.Point;
    import android.graphics.SurfaceTexture;
    import android.hardware.Camera;
    import android.hardware.camera2.CameraAccessException;
    import android.hardware.camera2.CameraCaptureSession;
    import android.hardware.camera2.CameraCharacteristics;
    import android.hardware.camera2.CameraDevice;
    import android.hardware.camera2.CameraManager;
    import android.hardware.camera2.CaptureRequest;
    import android.hardware.camera2.params.StreamConfigurationMap;
    import android.hardware.display.DisplayManager;
    import android.hardware.display.VirtualDisplay;
    import android.media.Image;
    import android.media.ImageReader;
    import android.media.MediaCodec;
    import android.media.MediaCodecInfo;
    import android.media.MediaCodecList;
    import android.media.MediaFormat;
    import android.media.projection.MediaProjection;
    import android.os.Build;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.HandlerThread;
    import android.util.DisplayMetrics;
    import android.util.Range;
    import android.util.Size;
    import android.view.Surface;
    import android.view.SurfaceHolder;
    import android.view.TextureView;
    
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import androidx.annotation.RequiresApi;
    import androidx.core.util.Pair;
    
    import java.nio.ByteBuffer;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    import cx.ring.daemon.IntVect;
    import cx.ring.daemon.Ringservice;
    import cx.ring.daemon.RingserviceJNI;
    import cx.ring.daemon.StringMap;
    import cx.ring.daemon.UintVect;
    import cx.ring.utils.Log;
    import cx.ring.views.AutoFitTextureView;
    import io.reactivex.Completable;
    import io.reactivex.Maybe;
    import io.reactivex.Observable;
    import io.reactivex.Single;
    import io.reactivex.schedulers.Schedulers;
    
    public class CameraService {
        private static final String TAG = CameraService.class.getSimpleName();
        private static final int FPS_MAX = 30;
        private static final int FPS_TARGET = 15;
        private static final Set<String> addedDevices = new HashSet<>();
        private final CameraManager manager;
    
        private final HashMap<String, VideoParams> mParams = new HashMap<>();
        private final Map<String, DeviceParams> mNativeParams = new HashMap<>();
        private final HandlerThread t = new HandlerThread("videoHandler");
        private Handler videoHandler = null;
        private CameraDevice previewCamera;
        private MediaProjection currentMediaProjection;
        private VirtualDisplay virtualDisplay;
        private MediaCodec currentCodec;
    
        CameraService(@NonNull Context c) {
            manager = (CameraManager) c.getSystemService(Context.CAMERA_SERVICE);
        }
    
        public Handler getVideoHandler() {
            if (t.getState() == Thread.State.NEW)
                t.start();
            if (videoHandler == null)
                videoHandler = new Handler(t.getLooper());
            return videoHandler;
        }
    
        static class VideoDevices {
            final List<String> cameras = new ArrayList<>();
            String currentId;
            int currentIndex;
            String cameraFront;
    
            String switchInput(boolean setDefaultCamera) {
                if(setDefaultCamera && !cameras.isEmpty()) {
                    currentId = cameras.get(0);
                }
                else if (!cameras.isEmpty()) {
                    currentIndex = (currentIndex + 1) % cameras.size();
                    currentId = cameras.get(currentIndex);
                } else {
                    currentId = null;
                }
                return currentId;
            }
        }
    
        protected VideoDevices devices = null;
        private VideoParams previewParams = null;
    
        public String switchInput(boolean setDefaultCamera) {
            if (devices == null)
                return null;
            return devices.switchInput(setDefaultCamera);
        }
    
        public VideoParams getParams(String camId) {
            if (camId != null) {
                return mParams.get(camId);
            } else if (previewParams != null) {
                return previewParams;
            } else if (devices != null && !devices.cameras.isEmpty()) {
                devices.currentId = devices.cameras.get(0);
                return mParams.get(devices.currentId);
            }
            return null;
        }
    
        public void setPreviewParams(VideoParams params) {
            previewParams = params;
        }
    
        public void setParameters(String camId, int format, int width, int height, int rate, int rotation) {
            DeviceParams deviceParams = mNativeParams.get(camId);
            if (deviceParams == null)
                return;
    
            CameraService.VideoParams params = mParams.get(camId);
            if (params == null) {
                params = new CameraService.VideoParams(camId, format, deviceParams.size.x, deviceParams.size.y, rate);
                mParams.put(camId, params);
            } else {
                params.id = camId;
                //params.format = format;
                params.width = deviceParams.size.x;
                params.height = deviceParams.size.y;
                params.rate = rate;
            }
            if (deviceParams.infos != null) {
                params.rotation = getCameraDisplayRotation(deviceParams, rotation);
            }
            Ringservice.setDeviceOrientation(camId, params.rotation);
        }
    
        public void setOrientation(int rotation) {
            for (String id : getCameraIds())
                setDeviceOrientation(id, rotation);
        }
    
        private void setDeviceOrientation(String camId, int screenRotation) {
            DeviceParams deviceParams = mNativeParams.get(camId);
            int rotation = 0;
            if (deviceParams != null && deviceParams.infos != null) {
                rotation = getCameraDisplayRotation(deviceParams, screenRotation);
            }
            CameraService.VideoParams params = mParams.get(camId);
            if (params != null) {
                params.rotation = rotation;
            }
            Ringservice.setDeviceOrientation(camId, rotation);
        }
    
        private static int getCameraDisplayRotation(DeviceParams device, int screenRotation) {
            return getCameraDisplayRotation(device.infos.orientation, rotationToDegrees(screenRotation), device.infos.facing);
        }
    
        private static int getCameraDisplayRotation(int sensorOrientation, int screenOrientation, int cameraFacing) {
            int rotation = 0;
            if (cameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                rotation = (sensorOrientation + screenOrientation + 360) % 360;
            } else {
                rotation = (sensorOrientation - screenOrientation + 360) % 360;
            }
            rotation = ((180 - rotation) + 180) % 360;
            return rotation;
        }
    
        public void getCameraInfo(String camId, IntVect formats, UintVect sizes, UintVect rates, Point minVideoSize) {
            Log.d(TAG, "getCameraInfo: " + camId + " min. size: " + minVideoSize);
            DeviceParams p = new CameraService.DeviceParams();
            p.size = new Point(0, 0);
            p.infos = new Camera.CameraInfo();
    
            rates.clear();
    
            fillCameraInfo(p, camId, formats, sizes, rates, minVideoSize);
            sizes.add((long) p.size.x);
            sizes.add((long) p.size.y);
            sizes.add((long) p.size.y);
            sizes.add((long) p.size.x);
    
            mNativeParams.put(camId, p);
        }
    
        public DeviceParams getNativeParams(String camId) {
            return mNativeParams.get(camId);
        }
    
        public boolean isPreviewFromFrontCamera() {
            return mNativeParams.size() == 1 || (devices != null && devices.currentId != null && devices.currentId.equals(devices.cameraFront));
        }
    
        public Map<String, StringMap> getPreviewSettings() {
            Map<String, StringMap> camSettings = new HashMap<>();
            for (String id : getCameraIds()) {
                CameraService.DeviceParams params = getNativeParams(id);
                if (params != null) {
                    camSettings.put(id, params.toMap());
                }
            }
            return camSettings;
        }
    
        public boolean hasCamera() {
            return getCameraCount() > 0;
        }
    
        public static class VideoParams {
            public String id;
            //public int format;
            // size as captured by Android
            public int width;
            public int height;
            public int rate;
            public int rotation;
            public String codec;
    
            public VideoParams(String id, int format, int width, int height, int rate) {
                this.id = id;
                //this.format = format;
                this.width = width;
                this.height = height;
                this.rate = rate;
            }
    
            public String getCodec() {
                if (codec == null)
                    return MediaFormat.MIMETYPE_VIDEO_AVC;
                switch (codec) {
                    case "H264": return MediaFormat.MIMETYPE_VIDEO_AVC;
                    case "H265": return MediaFormat.MIMETYPE_VIDEO_HEVC;
                    case "VP8": return MediaFormat.MIMETYPE_VIDEO_VP8;
                    case "VP9": return MediaFormat.MIMETYPE_VIDEO_VP9;
                    case "MP4V-ES": return MediaFormat.MIMETYPE_VIDEO_MPEG4;
                }
                return codec;
            }
        }
    
        static class DeviceParams {
            Point size;
            long rate;
            Camera.CameraInfo infos;
    
            StringMap toMap() {
                StringMap map = new StringMap();
                map.put("size", size.x + "x" + size.y);
                map.put("rate", Long.toString(rate));
                return map;
            }
        }
    
        static private Observable<Pair<String, CameraCharacteristics>> filterCompatibleCamera(String[] cameraIds, CameraManager cameraManager) {
            return Observable.fromArray(cameraIds)
                    .map(id -> new Pair<>(id, cameraManager.getCameraCharacteristics(id)))
                    .filter(camera -> {
                        try {
                            for (int c : camera.second.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES))
                                if (c == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE)
                                    return true;
                        } catch (Exception e) {
                            return false;
                        }
                        return false;
                    });
        }
    
        static private Observable<String> filterCameraIdsFacing(List<Pair<String, CameraCharacteristics>> cameras, int facing) {
            return Observable.fromIterable(cameras)
                    .filter(camera -> camera.second.get(CameraCharacteristics.LENS_FACING) == facing)
                    .map(camera -> camera.first);
        }
    
        private Single<VideoDevices> loadDevices(CameraManager manager) {
            return Single.fromCallable(() -> {
                VideoDevices devices = new VideoDevices();
                List<Pair<String, CameraCharacteristics>> cameras = filterCompatibleCamera(manager.getCameraIdList(), manager).toList().blockingGet();
                Maybe<String> backCamera = filterCameraIdsFacing(cameras, CameraCharacteristics.LENS_FACING_BACK).firstElement();
                Maybe<String> frontCamera = filterCameraIdsFacing(cameras, CameraCharacteristics.LENS_FACING_FRONT).firstElement();
                Observable<String> externalCameras;
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                    externalCameras = filterCameraIdsFacing(cameras, CameraCharacteristics.LENS_FACING_EXTERNAL);
                } else  {
                    externalCameras = Observable.empty();
                }
                Observable.concat(
                        frontCamera.toObservable(),
                        backCamera.toObservable(),
                        externalCameras).blockingSubscribe(devices.cameras::add);
                if (!devices.cameras.isEmpty())
                    devices.currentId = devices.cameras.get(0);
                devices.cameraFront = frontCamera.blockingGet();
                Log.w(TAG, "Loading video devices: found " + devices.cameras.size());
                return devices;
            }).subscribeOn(Schedulers.io());
        }
    
        Completable init() {
            if (manager == null)
                return Completable.error(new IllegalStateException("Video manager not available"));
            return loadDevices(manager)
                    .map(devs -> {
                        synchronized (addedDevices) {
                            VideoDevices old = devices;
                            devices = devs;
                            // Removed devices
                            if (old != null) {
                                for (String oldId : old.cameras) {
                                    if (!devs.cameras.contains(oldId)) {
                                        if (addedDevices.remove(oldId))
                                            RingserviceJNI.removeVideoDevice(oldId);
                                    }
                                }
                            }
                            // Added devices
                            for (String camera : devs.cameras) {
                                if (addedDevices.add(camera))
                                    RingserviceJNI.addVideoDevice(camera);
                            }
                            // New default
                            if (devs.currentId != null) {
                                RingserviceJNI.setDefaultDevice(devs.currentId);
                            }
                        }
                        return devs;
                    })
                    .ignoreElement()
                    .doOnError(e -> Log.e(TAG, "Error initializing video device", e))
                    .onErrorComplete();
        }
    
        interface CameraListener {
            void onOpened();
            void onError();
        }
    
        public void closeCamera() {
            CameraDevice camera = previewCamera;
            if (camera != null) {
                previewCamera = null;
                camera.close();
                currentCodec = null;
            }
        }
    
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        private static void listSupportedCodecs(MediaCodecList list) {
            try {
                for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
                    for (String type : codecInfo.getSupportedTypes()) {
                        try {
                            MediaCodecInfo.CodecCapabilities codecCaps = codecInfo.getCapabilitiesForType(type);
                            MediaCodecInfo.EncoderCapabilities caps = codecCaps.getEncoderCapabilities();
                            if (caps == null)
                                continue;
                            MediaCodecInfo.VideoCapabilities video_caps = codecCaps.getVideoCapabilities();
                            if (video_caps == null)
                                continue;
                            Log.w(TAG, "Codec info:" + codecInfo.getName() + " type: " + type);
                            Log.w(TAG, "Encoder capabilities: complexityRange: " + caps.getComplexityRange());
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                                Log.w(TAG, "Encoder capabilities: qualityRange: " + caps.getQualityRange());
                            }
                            Log.w(TAG, "Encoder capabilities: VBR: " + caps.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR));
                            Log.w(TAG, "Encoder capabilities: CBR: " + caps.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR));
                            Log.w(TAG, "Encoder capabilities: CQ: " + caps.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ));
                            Log.w(TAG, "Bitrate range: " + video_caps.getBitrateRange());
                            for (int format : codecCaps.colorFormats) {
                                Log.w(TAG, "Supported color format: " + format);
                            }
    
                            Range<Integer> widths = video_caps.getSupportedWidths();
                            Range<Integer> heights = video_caps.getSupportedHeights();
                            Log.w(TAG, "Supported sizes: " + widths.getLower() + "x" + heights.getLower() + " -> " + widths.getUpper() + "x" + heights.getUpper());
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                Log.w(TAG, "AchievableFrameRates: " + video_caps.getAchievableFrameRatesFor(1920, 1080));
                            }
                            Log.w(TAG, "SupportedFrameRates: " + video_caps.getSupportedFrameRatesFor(/*widths.getUpper(), heights.getUpper()*/1920, 1080));
    
                            for (MediaCodecInfo.CodecProfileLevel profileLevel : codecCaps.profileLevels)
                                Log.w(TAG, "profileLevels: " + profileLevel);
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                                Log.w(TAG, "FEATURE_IntraRefresh: " + codecCaps.isFeatureSupported(MediaCodecInfo.CodecCapabilities.FEATURE_IntraRefresh));
                            }
                        } catch (Exception e) {
                            Log.w(TAG, "Can't query codec info", e);
                        }
                    }
                }
            } catch (Exception e) {
                Log.w(TAG, "Can't query codec info", e);
            }
        }
    
        public static Pair<MediaCodec, Surface> openEncoder(VideoParams videoParams, String mimeType, Handler handler, int resolution, int bitrate) {
            Log.d(TAG, "Video with codec " + mimeType + " resolution: " + videoParams.width + "x" + videoParams.height + " Bitrate: " + bitrate);
            int bitrateValue;
            if(bitrate == 0)
                bitrateValue = resolution >= 720 ? 192 * 8 * 1024 : 100 * 8 * 1024;
            else
                bitrateValue = bitrate * 8 * 1024;
    
            int frameRate = 30; // 30 fps
    
            MediaFormat format = MediaFormat.createVideoFormat(mimeType, videoParams.width, videoParams.height);
            format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
            format.setInteger(MediaFormat.KEY_BIT_RATE, bitrateValue);
            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            if (Build.VERSION.SDK_INT != Build.VERSION_CODES.LOLLIPOP)
                format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
            format.setInteger(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / frameRate);
            /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, -1);
                format.setInteger(MediaFormat.KEY_INTRA_REFRESH_PERIOD, 5);
            } else {
                format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
            }*/
            format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
            //format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.CodecCapabilities.BITRATE_MODE_VBR);
    
            MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
            //listSupportedCodecs(codecs);
            String codecName = codecs.findEncoderForFormat(format);
    
            format.setInteger(MediaFormat.KEY_FRAME_RATE, 24);
    
            Surface encoderInput = null;
            MediaCodec codec = null;
            if (codecName != null) {
                try {
                    codec = MediaCodec.createByCodecName(codecName);
                    Bundle params = new Bundle();
                    params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrateValue);
                    codec.setParameters(params);
                    codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
                    encoderInput = codec.createInputSurface();
                    MediaCodec.Callback callback = new MediaCodec.Callback() {
                        @Override
                        public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {}
    
                        @Override
                        public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
                            try {
                                if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == 0) {
                                    ByteBuffer buffer = codec.getOutputBuffer(index);
                                    RingserviceJNI.captureVideoPacket(buffer, info.size, info.offset, (info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0, info.presentationTimeUs, videoParams.rotation);
                                    codec.releaseOutputBuffer(index, false);
                                }
                            } catch (IllegalStateException e) {
                                Log.e(TAG, "MediaCodec can't process buffer", e);
                            }
                        }
    
                        @Override
                        public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
                            Log.e(TAG, "MediaCodec onError", e);
                        }
    
                        @Override
                        public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
                            Log.e(TAG, "MediaCodec onOutputFormatChanged " + format);
                        }
                    };
    
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        setCodecCallback(codec, callback, handler);
                    } else {
                        setCodecCallback(codec, callback);
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Can't open codec", e);
                    if (codec != null) {
                        codec.release();
                        codec = null;
                    }
                    if (encoderInput != null) {
                        encoderInput.release();
                        encoderInput = null;
                    }
                }
            }
            return Pair.create(codec, encoderInput);
        }
    
        private static void setCodecCallback(@NonNull MediaCodec codec, MediaCodec.Callback callback) {
            codec.setCallback(callback);
        }
    
        @RequiresApi(api = Build.VERSION_CODES.M)
        private static void setCodecCallback(@NonNull MediaCodec codec, MediaCodec.Callback callback, Handler handler) {
            codec.setCallback(callback, handler);
        }
    
        public void requestKeyFrame() {
            Log.w(TAG, "requestKeyFrame()");
            try {
                if (currentCodec != null) {
                    Bundle params = new Bundle();
                    params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
                    currentCodec.setParameters(params);
                }
            } catch (IllegalStateException e) {
                Log.w(TAG, "Can't send keyframe request", e);
            }
        }
    
        public void setBitrate(int bitrate) {
            Log.w(TAG, "setBitrate() " + bitrate);
            try {
                if (currentCodec != null) {
                    Bundle params = new Bundle();
                    params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate * 1024);
                    currentCodec.setParameters(params);
                }
            } catch (IllegalStateException e) {
                Log.w(TAG, "Can't set bitrate", e);
            }
        }
    
        private static @NonNull Size chooseOptimalSize(@Nullable Size[] choices, int textureViewWidth, int textureViewHeight, int maxWidth, int maxHeight, Size target) {
            if (choices == null)
                return target;
            // Collect the supported resolutions that are at least as big as the preview Surface
            List<Size> bigEnough = new ArrayList<>();
            // Collect the supported resolutions that are smaller than the preview Surface
            List<Size> notBigEnough = new ArrayList<>();
            int w = target.getWidth();
            int h = target.getHeight();
            for (Size option : choices) {
                Log.w(TAG, "supportedSize: " + option);
                if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
                        option.getHeight() == option.getWidth() * h / w) {
                    if (option.getWidth() >= textureViewWidth &&
                            option.getHeight() >= textureViewHeight) {
                        bigEnough.add(option);
                    } else {
                        notBigEnough.add(option);
                    }
                }
            }
    
            // Pick the smallest of those big enough. If there is no one big enough, pick the
            // largest of those not big enough.
            if (bigEnough.size() > 0) {
                return Collections.min(bigEnough, new CompareSizesByArea());
            } else if (notBigEnough.size() > 0) {
                return Collections.max(notBigEnough, new CompareSizesByArea());
            } else {
                android.util.Log.e(TAG, "Couldn't find any suitable preview size");
                return choices[0];
            }
        }
    
        private static @NonNull Range<Integer> chooseOptimalFpsRange(Range<Integer>[] ranges) {
            Range<Integer> range = null;
            if (ranges != null && ranges.length > 0) {
                for (Range<Integer> r : ranges) {
                    if (r.getUpper() > FPS_MAX)
                        continue;
                    if (range != null) {
                        int d = Math.abs(r.getUpper() - FPS_TARGET) - Math.abs(range.getUpper() - FPS_TARGET);
                        if (d > 0)
                            continue;
                        if (d == 0 && r.getLower() > range.getLower())
                            continue;
                    }
                    range = r;
                }
                if (range == null)
                    range = ranges[0];
            }
            return range == null ? new Range<>(FPS_TARGET, FPS_TARGET) : range;
        }
    
        private Pair<MediaCodec, VirtualDisplay> createVirtualDisplay(MediaProjection projection, DisplayMetrics metrics) {
            int screenWidth = metrics.widthPixels;
            int screenHeight = metrics.heightPixels;
            int screenDensity = metrics.densityDpi;
    
            Handler handler = getVideoHandler();
    
            Pair<MediaCodec, Surface> r = null;
            while (screenWidth >= 320) {
                CameraService.VideoParams params = new CameraService.VideoParams(null, 0, screenWidth, screenHeight, 24);
                r = CameraService.openEncoder(params, MediaFormat.MIMETYPE_VIDEO_AVC, handler, 720, 0);
                if (r.first == null) {
                    screenWidth /= 2;
                    screenHeight /= 2;
                } else
                    break;
            }
    
            final Surface surface = r.second;
            final MediaCodec codec = r.first;
            if (codec != null)
                codec.start();
            try {
                return Pair.create(codec, projection.createVirtualDisplay("ScreenSharingDemo",
                        screenWidth, screenHeight, screenDensity,
                        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface
                        , new VirtualDisplay.Callback() {
                            @Override
                            public void onPaused() {
                                Log.w(TAG, "VirtualDisplay.onPaused");
                            }
    
                            @Override
                            public void onResumed() {
                                Log.w(TAG, "VirtualDisplay.onResumed");
                            }
    
                            @Override
                            public void onStopped() {
                                Log.w(TAG, "VirtualDisplay.onStopped");
                                if (surface != null) {
                                    surface.release();
                                    if (codec != null)
                                        codec.release();
                                    if (currentCodec == codec)
                                        currentCodec = null;
                                }
                            }
                        }, handler));
            } catch (Exception e) {
                if (codec != null) {
                    codec.stop();
                    codec.release();
                }
                if (surface != null) {
                    surface.release();
                }
                return null;
            }
        }
    
        boolean startScreenSharing(MediaProjection mediaProjection, DisplayMetrics metrics) {
            Pair<MediaCodec, VirtualDisplay> r = createVirtualDisplay(mediaProjection, metrics);
            if (r != null)  {
                currentMediaProjection = mediaProjection;
                currentCodec = r.first;
                virtualDisplay = r.second;
                return true;
            }
            return false;
        }
    
        void stopScreenSharing() {
            if (virtualDisplay != null) {
                virtualDisplay.release();
                virtualDisplay = null;
            }
            if (currentMediaProjection != null) {
                currentMediaProjection.stop();
                currentMediaProjection = null;
            }
        }
    
        void openCamera(VideoParams videoParams, TextureView surface, CameraListener listener, boolean hw_accel, int resolution, int bitrate) {
            CameraDevice camera = previewCamera;
            if (camera != null) {
                camera.close();
            }
    
            if (manager == null)
                return;
            Handler handler = getVideoHandler();
            try {
                AutoFitTextureView view = (AutoFitTextureView) surface;
                boolean flip = videoParams.rotation % 180 != 0;
    
                CameraCharacteristics cc = manager.getCameraCharacteristics(videoParams.id);
                final Range<Integer> fpsRange = chooseOptimalFpsRange(cc.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES));
    
                StreamConfigurationMap streamConfigs = cc.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                final Size previewSize = chooseOptimalSize(streamConfigs == null ? null : streamConfigs.getOutputSizes(SurfaceHolder.class),
                        flip ? view.getHeight() : view.getWidth(), flip ? view.getWidth() : view.getHeight(),
                        videoParams.width, videoParams.height,
                        new Size(videoParams.width, videoParams.height));
                Log.d(TAG, "Selected preview size: " + previewSize + ", fps range: " + fpsRange + " rate: "+videoParams.rate);
                view.setAspectRatio(previewSize.getHeight(), previewSize.getWidth());
    
                SurfaceTexture texture = view.getSurfaceTexture();
                Surface s = new Surface(texture);
    
                final Pair<MediaCodec, Surface> codec = (hw_accel && videoParams.getCodec() != null) ? openEncoder(videoParams, videoParams.getCodec(), handler, resolution, bitrate) : null;
    
                final List<Surface> targets = new ArrayList<>(2);
                targets.add(s);
                ImageReader tmpReader = null;
                if (codec != null && codec.second != null) {
                    targets.add(codec.second);
                } else {
                    tmpReader = ImageReader.newInstance(videoParams.width, videoParams.height, ImageFormat.YUV_420_888, 8);
                    tmpReader.setOnImageAvailableListener(r -> {
                        Image image = r.acquireLatestImage();
                        if (image != null)
                            RingserviceJNI.captureVideoFrame(image, videoParams.rotation);
                    }, handler);
                    targets.add(tmpReader.getSurface());
                }
                final ImageReader reader = tmpReader;
                final boolean[] codecStarted = {false};
    
                manager.openCamera(videoParams.id, new CameraDevice.StateCallback() {
                    @Override
                    public void onOpened(@NonNull CameraDevice camera) {
                        try {
                            Log.w(TAG, "onOpened");
                            previewCamera = camera;
                            texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
                            CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                            builder.addTarget(s);
                            if (codec != null && codec.second != null) {
                                builder.addTarget(codec.second);
                            } else if (reader != null) {
                                builder.addTarget(reader.getSurface());
                            }
                            builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
                            builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
                            builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
                            builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
                            final CaptureRequest request = builder.build();
    
                            camera.createCaptureSession(targets, new CameraCaptureSession.StateCallback() {
                                @Override
                                public void onConfigured(@NonNull CameraCaptureSession session) {
                                    Log.w(TAG, "onConfigured");
                                    listener.onOpened();
                                    try {
                                        session.setRepeatingRequest(request, (codec != null && codec.second != null) ? new CameraCaptureSession.CaptureCallback() {
                                            @Override
                                            public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
                                                if (frameNumber == 1) {
                                                    try {
                                                        codec.first.start();
                                                        codecStarted[0] = true;
                                                    } catch (Exception e) {
                                                        listener.onError();
                                                    }
                                                }
                                            }
                                        } : null, handler);
                                        if (codec != null && codec.first != null) {
                                            currentCodec = codec.first;
                                        }
                                    } catch (Exception e) {
                                        Log.w(TAG, "onConfigured error:", e);
                                        camera.close();
                                    }
                                }
    
                                @Override
                                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                                    listener.onError();
                                    Log.w(TAG, "onConfigureFailed");
                                }
    
                                @Override
                                public void onClosed(@NonNull CameraCaptureSession session) {
                                    Log.w(TAG, "CameraCaptureSession onClosed");
                                }
                            }, handler);
                        } catch (Exception e) {
                            Log.w(TAG, "onOpened error:", e);
                        }
                    }
    
                    @Override
                    public void onDisconnected(@NonNull CameraDevice camera) {
                        Log.w(TAG, "onDisconnected");
                        camera.close();
                        listener.onError();
                    }
    
                    @Override
                    public void onError(@NonNull CameraDevice camera, int error) {
                        Log.w(TAG, "onError: " + error);
                        camera.close();
                        listener.onError();
                    }
    
                    @Override
                    public void onClosed(@NonNull CameraDevice camera) {
                        Log.w(TAG, "onClosed");
                        if (previewCamera == camera)
                            previewCamera = null;
                        if (codec != null) {
                            if (codec.first != null) {
                                if (codecStarted[0])
                                    codec.first.signalEndOfInputStream();
                                codec.first.release();
                                if (codec.first == currentCodec)
                                    currentCodec = null;
                            }
                            if (codec.second != null)
                                codec.second.release();
                        }
                        if (reader != null) {
                            reader.close();
                        }
                        s.release();
                    }
                }, handler);
            } catch (SecurityException e) {
                Log.e(TAG, "Security exception while settings preview parameters", e);
            } catch (Exception e) {
                Log.e(TAG, "Exception while settings preview parameters", e);
            }
        }
    
        public boolean isOpen() {
            return previewCamera != null;
        }
    
        List<String> getCameraIds() {
            return devices == null ? new ArrayList<>() : devices.cameras;
        }
    
        public int getCameraCount() {
            try {
                return devices == null ? manager.getCameraIdList().length : devices.cameras.size();
            } catch (CameraAccessException e) {
                return 0;
            }
        }
    
        void fillCameraInfo(DeviceParams p, String camId, IntVect formats, UintVect sizes, UintVect rates, Point minVideoSize) {
            if (manager == null)
                return;
            try {
                final CameraCharacteristics cc = manager.getCameraCharacteristics(camId);
                StreamConfigurationMap streamConfigs = cc.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                if (streamConfigs == null)
                    return;
                Size[] rawSizes = streamConfigs.getOutputSizes(ImageFormat.YUV_420_888);
                Size newSize = rawSizes[0];
                for (Size s : rawSizes) {
                    if (s.getWidth() < s.getHeight()) {
                        continue;
                    }
                    if ((s.getWidth() == minVideoSize.x && s.getHeight() == minVideoSize.y)
                            // Has height closer but still higher than target
                            || (newSize.getHeight() < minVideoSize.y
                                ? s.getHeight() > newSize.getHeight()
                                : (s.getHeight() >= minVideoSize.y && s.getHeight() < newSize.getHeight()))
                            // Has width closer but still higher than target
                            || (s.getHeight() == newSize.getHeight()
                                && newSize.getWidth() < minVideoSize.x
                                        ? s.getWidth() > newSize.getWidth()
                                        : (s.getWidth() >= minVideoSize.x && s.getWidth() < newSize.getWidth()))) {
                        newSize = s;
                    }
                }
                p.size.x = newSize.getWidth();
                p.size.y = newSize.getHeight();
    
                long minDuration = streamConfigs.getOutputMinFrameDuration(ImageFormat.YUV_420_888, newSize);
                double maxfps = 1000e9d / minDuration;
                long fps = (long) maxfps;
                rates.add(fps);
                p.rate = fps;
    
                int facing = cc.get(CameraCharacteristics.LENS_FACING);
                p.infos.orientation = cc.get(CameraCharacteristics.SENSOR_ORIENTATION);
                p.infos.facing = facing == CameraCharacteristics.LENS_FACING_FRONT ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
            } catch (Exception e) {
                Log.e(TAG, "An error occurred getting camera info", e);
            }
        }
    
        static private int rotationToDegrees(int rotation) {
            switch (rotation) {
                case Surface.ROTATION_0:
                    return 0;
                case Surface.ROTATION_90:
                    return 90;
                case Surface.ROTATION_180:
                    return 180;
                case Surface.ROTATION_270:
                    return 270;
            }
            return 0;
        }
    
        /**
         * Compares two {@code Size}s based on their areas.
         */
        static class CompareSizesByArea implements Comparator<Size> {
            @Override
            public int compare(Size lhs, Size rhs) {
                // We cast here to ensure the multiplications won't overflow
                return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
                        (long) rhs.getWidth() * rhs.getHeight());
            }
        }
    }