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