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 this.videoSource = peerConnectionFactory.createVideoSource(false);
61 this.cameraVideoCapturer.initialize(
62 surfaceTextureHelper, context, this.videoSource.getCapturerObserver());
63 }
64
65 public VideoSource getVideoSource() {
66 final VideoSource videoSource = this.videoSource;
67 if (videoSource == null) {
68 throw new IllegalStateException("VideoSourceWrapper was not initialized");
69 }
70 return videoSource;
71 }
72
73 public void startCapture() {
74 final int frameRate = getFrameRate();
75 Log.d(
76 Config.LOGTAG,
77 String.format(
78 "start capturing at %dx%d@%d",
79 captureFormat.width, captureFormat.height, frameRate));
80 this.cameraVideoCapturer.startCapture(captureFormat.width, captureFormat.height, frameRate);
81 }
82
83 public void stopCapture() throws InterruptedException {
84 this.cameraVideoCapturer.stopCapture();
85 }
86
87 public void dispose() {
88 this.cameraVideoCapturer.dispose();
89 if (this.videoSource != null) {
90 dispose(this.videoSource);
91 }
92 }
93
94 private static void dispose(final VideoSource videoSource) {
95 try {
96 videoSource.dispose();
97 } catch (final IllegalStateException e) {
98 Log.e(Config.LOGTAG, "unable to dispose video source", e);
99 }
100 }
101
102 public ListenableFuture<Boolean> switchCamera() {
103 final SettableFuture<Boolean> future = SettableFuture.create();
104 this.cameraVideoCapturer.switchCamera(
105 new CameraVideoCapturer.CameraSwitchHandler() {
106 @Override
107 public void onCameraSwitchDone(final boolean isFrontCamera) {
108 VideoSourceWrapper.this.isFrontCamera = isFrontCamera;
109 future.set(isFrontCamera);
110 }
111
112 @Override
113 public void onCameraSwitchError(final String message) {
114 future.setException(
115 new IllegalStateException(
116 String.format("Unable to switch camera %s", message)));
117 }
118 });
119 return future;
120 }
121
122 public boolean isFrontCamera() {
123 return this.isFrontCamera;
124 }
125
126 public boolean isCameraSwitchable() {
127 return this.availableCameras.size() > 1;
128 }
129
130 public static class Factory {
131 final Context context;
132
133 public Factory(final Context context) {
134 this.context = context;
135 }
136
137 public VideoSourceWrapper create() {
138 final CameraEnumerator enumerator = new Camera2Enumerator(context);
139 final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames());
140 for (final String deviceName : deviceNames) {
141 if (isFrontFacing(enumerator, deviceName)) {
142 final VideoSourceWrapper videoSourceWrapper =
143 of(enumerator, deviceName, deviceNames);
144 if (videoSourceWrapper == null) {
145 return null;
146 }
147 videoSourceWrapper.isFrontCamera = true;
148 return videoSourceWrapper;
149 }
150 }
151 if (deviceNames.size() == 0) {
152 return null;
153 } else {
154 return of(enumerator, Iterables.get(deviceNames, 0), deviceNames);
155 }
156 }
157
158 @Nullable
159 private VideoSourceWrapper of(
160 final CameraEnumerator enumerator,
161 final String deviceName,
162 final Set<String> availableCameras) {
163 final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
164 if (capturer == null) {
165 return null;
166 }
167 final ArrayList<CameraEnumerationAndroid.CaptureFormat> choices =
168 new ArrayList<>(enumerator.getSupportedFormats(deviceName));
169 Collections.sort(choices, (a, b) -> b.width - a.width);
170 for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) {
171 if (captureFormat.width <= CAPTURING_RESOLUTION) {
172 return new VideoSourceWrapper(capturer, captureFormat, availableCameras);
173 }
174 }
175 return null;
176 }
177
178 private static boolean isFrontFacing(
179 final CameraEnumerator cameraEnumerator, final String deviceName) {
180 try {
181 return cameraEnumerator.isFrontFacing(deviceName);
182 } catch (final NullPointerException e) {
183 return false;
184 }
185 }
186 }
187}