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