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 this.videoSource.dispose();
91 }
92 }
93
94 public ListenableFuture<Boolean> switchCamera() {
95 final SettableFuture<Boolean> future = SettableFuture.create();
96 this.cameraVideoCapturer.switchCamera(
97 new CameraVideoCapturer.CameraSwitchHandler() {
98 @Override
99 public void onCameraSwitchDone(final boolean isFrontCamera) {
100 VideoSourceWrapper.this.isFrontCamera = isFrontCamera;
101 future.set(isFrontCamera);
102 }
103
104 @Override
105 public void onCameraSwitchError(final String message) {
106 future.setException(
107 new IllegalStateException(
108 String.format("Unable to switch camera %s", message)));
109 }
110 });
111 return future;
112 }
113
114 public boolean isFrontCamera() {
115 return this.isFrontCamera;
116 }
117
118 public boolean isCameraSwitchable() {
119 return this.availableCameras.size() > 1;
120 }
121
122 public static class Factory {
123 final Context context;
124
125 public Factory(final Context context) {
126 this.context = context;
127 }
128
129 public VideoSourceWrapper create() {
130 final CameraEnumerator enumerator = new Camera2Enumerator(context);
131 final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames());
132 for (final String deviceName : deviceNames) {
133 if (isFrontFacing(enumerator, deviceName)) {
134 final VideoSourceWrapper videoSourceWrapper =
135 of(enumerator, deviceName, deviceNames);
136 if (videoSourceWrapper == null) {
137 return null;
138 }
139 videoSourceWrapper.isFrontCamera = true;
140 return videoSourceWrapper;
141 }
142 }
143 if (deviceNames.size() == 0) {
144 return null;
145 } else {
146 return of(enumerator, Iterables.get(deviceNames, 0), deviceNames);
147 }
148 }
149
150 @Nullable
151 private VideoSourceWrapper of(
152 final CameraEnumerator enumerator,
153 final String deviceName,
154 final Set<String> availableCameras) {
155 final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
156 if (capturer == null) {
157 return null;
158 }
159 final ArrayList<CameraEnumerationAndroid.CaptureFormat> choices =
160 new ArrayList<>(enumerator.getSupportedFormats(deviceName));
161 Collections.sort(choices, (a, b) -> b.width - a.width);
162 for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) {
163 if (captureFormat.width <= CAPTURING_RESOLUTION) {
164 return new VideoSourceWrapper(capturer, captureFormat, availableCameras);
165 }
166 }
167 return null;
168 }
169
170 private static boolean isFrontFacing(
171 final CameraEnumerator cameraEnumerator, final String deviceName) {
172 try {
173 return cameraEnumerator.isFrontFacing(deviceName);
174 } catch (final NullPointerException e) {
175 return false;
176 }
177 }
178 }
179}