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}