1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.content.Context;
  4import android.util.Log;
  5import androidx.annotation.Nullable;
  6import com.google.common.collect.ImmutableSet;
  7import com.google.common.collect.Iterables;
  8import com.google.common.util.concurrent.ListenableFuture;
  9import com.google.common.util.concurrent.SettableFuture;
 10import eu.siacs.conversations.Config;
 11import java.util.ArrayList;
 12import java.util.Collections;
 13import java.util.Set;
 14import org.webrtc.Camera2Enumerator;
 15import org.webrtc.CameraEnumerationAndroid;
 16import org.webrtc.CameraEnumerator;
 17import org.webrtc.CameraVideoCapturer;
 18import org.webrtc.EglBase;
 19import org.webrtc.PeerConnectionFactory;
 20import org.webrtc.SurfaceTextureHelper;
 21import org.webrtc.VideoSource;
 22
 23class VideoSourceWrapper {
 24
 25    private static final int CAPTURING_RESOLUTION = 1920;
 26    private static final int CAPTURING_MAX_FRAME_RATE = 30;
 27
 28    private final CameraVideoCapturer cameraVideoCapturer;
 29    private final CameraEnumerationAndroid.CaptureFormat captureFormat;
 30    private final Set<String> availableCameras;
 31    private boolean isFrontCamera = false;
 32    private VideoSource videoSource;
 33
 34    VideoSourceWrapper(
 35            CameraVideoCapturer cameraVideoCapturer,
 36            CameraEnumerationAndroid.CaptureFormat captureFormat,
 37            Set<String> cameras) {
 38        this.cameraVideoCapturer = cameraVideoCapturer;
 39        this.captureFormat = captureFormat;
 40        this.availableCameras = cameras;
 41    }
 42
 43    private int getFrameRate() {
 44        return Math.max(
 45                captureFormat.framerate.min,
 46                Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max));
 47    }
 48
 49    public void initialize(
 50            final PeerConnectionFactory peerConnectionFactory,
 51            final Context context,
 52            final EglBase.Context eglBaseContext) {
 53        final SurfaceTextureHelper surfaceTextureHelper =
 54                SurfaceTextureHelper.create("webrtc", eglBaseContext);
 55        if (surfaceTextureHelper == null) {
 56            throw new IllegalStateException("Could not create SurfaceTextureHelper");
 57        }
 58        this.videoSource = peerConnectionFactory.createVideoSource(false);
 59        this.cameraVideoCapturer.initialize(
 60                surfaceTextureHelper, context, this.videoSource.getCapturerObserver());
 61    }
 62
 63    public VideoSource getVideoSource() {
 64        final VideoSource videoSource = this.videoSource;
 65        if (videoSource == null) {
 66            throw new IllegalStateException("VideoSourceWrapper was not initialized");
 67        }
 68        return videoSource;
 69    }
 70
 71    public void startCapture() {
 72        final int frameRate = getFrameRate();
 73        Log.d(
 74                Config.LOGTAG,
 75                String.format(
 76                        "start capturing at %dx%d@%d",
 77                        captureFormat.width, captureFormat.height, frameRate));
 78        this.cameraVideoCapturer.startCapture(captureFormat.width, captureFormat.height, frameRate);
 79    }
 80
 81    public void stopCapture() throws InterruptedException {
 82        this.cameraVideoCapturer.stopCapture();
 83    }
 84
 85    public void dispose() {
 86        this.cameraVideoCapturer.dispose();
 87        if (this.videoSource != null) {
 88            dispose(this.videoSource);
 89        }
 90    }
 91
 92    private static void dispose(final VideoSource videoSource) {
 93        try {
 94            videoSource.dispose();
 95        } catch (final IllegalStateException e) {
 96            Log.e(Config.LOGTAG, "unable to dispose video source", e);
 97        }
 98    }
 99
100    public ListenableFuture<Boolean> switchCamera() {
101        final SettableFuture<Boolean> future = SettableFuture.create();
102        this.cameraVideoCapturer.switchCamera(
103                new CameraVideoCapturer.CameraSwitchHandler() {
104                    @Override
105                    public void onCameraSwitchDone(final boolean isFrontCamera) {
106                        VideoSourceWrapper.this.isFrontCamera = isFrontCamera;
107                        future.set(isFrontCamera);
108                    }
109
110                    @Override
111                    public void onCameraSwitchError(final String message) {
112                        future.setException(
113                                new IllegalStateException(
114                                        String.format("Unable to switch camera %s", message)));
115                    }
116                });
117        return future;
118    }
119
120    public boolean isFrontCamera() {
121        return this.isFrontCamera;
122    }
123
124    public boolean isCameraSwitchable() {
125        return this.availableCameras.size() > 1;
126    }
127
128    public static class Factory {
129        final Context context;
130
131        public Factory(final Context context) {
132            this.context = context;
133        }
134
135        public VideoSourceWrapper create() {
136            final CameraEnumerator enumerator = new Camera2Enumerator(context);
137            final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames());
138            for (final String deviceName : deviceNames) {
139                if (isFrontFacing(enumerator, deviceName)) {
140                    final VideoSourceWrapper videoSourceWrapper =
141                            of(enumerator, deviceName, deviceNames);
142                    if (videoSourceWrapper == null) {
143                        return null;
144                    }
145                    videoSourceWrapper.isFrontCamera = true;
146                    return videoSourceWrapper;
147                }
148            }
149            if (deviceNames.isEmpty()) {
150                return null;
151            } else {
152                return of(enumerator, Iterables.get(deviceNames, 0), deviceNames);
153            }
154        }
155
156        @Nullable
157        private VideoSourceWrapper of(
158                final CameraEnumerator enumerator,
159                final String deviceName,
160                final Set<String> availableCameras) {
161            final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
162            if (capturer == null) {
163                return null;
164            }
165            final ArrayList<CameraEnumerationAndroid.CaptureFormat> choices =
166                    new ArrayList<>(enumerator.getSupportedFormats(deviceName));
167            Collections.sort(choices, (a, b) -> b.width - a.width);
168            for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) {
169                if (captureFormat.width <= CAPTURING_RESOLUTION) {
170                    return new VideoSourceWrapper(capturer, captureFormat, availableCameras);
171                }
172            }
173            return null;
174        }
175
176        private static boolean isFrontFacing(
177                final CameraEnumerator cameraEnumerator, final String deviceName) {
178            try {
179                return cameraEnumerator.isFrontFacing(deviceName);
180            } catch (final NullPointerException e) {
181                return false;
182            }
183        }
184    }
185}