1package eu.siacs.conversations.xmpp.jingle;
2
3import android.content.Context;
4import android.os.Handler;
5import android.os.Looper;
6import android.util.Log;
7
8import com.google.common.collect.ImmutableList;
9import com.google.common.util.concurrent.Futures;
10import com.google.common.util.concurrent.ListenableFuture;
11import com.google.common.util.concurrent.MoreExecutors;
12import com.google.common.util.concurrent.SettableFuture;
13
14import org.webrtc.AudioSource;
15import org.webrtc.AudioTrack;
16import org.webrtc.Camera1Capturer;
17import org.webrtc.Camera1Enumerator;
18import org.webrtc.CameraVideoCapturer;
19import org.webrtc.CandidatePairChangeEvent;
20import org.webrtc.DataChannel;
21import org.webrtc.IceCandidate;
22import org.webrtc.MediaConstraints;
23import org.webrtc.MediaStream;
24import org.webrtc.PeerConnection;
25import org.webrtc.PeerConnectionFactory;
26import org.webrtc.RtpReceiver;
27import org.webrtc.SdpObserver;
28import org.webrtc.SessionDescription;
29import org.webrtc.VideoCapturer;
30import org.webrtc.VideoSource;
31import org.webrtc.VideoTrack;
32
33import java.util.List;
34import java.util.Set;
35
36import javax.annotation.Nonnull;
37import javax.annotation.Nullable;
38
39import eu.siacs.conversations.Config;
40import eu.siacs.conversations.services.AppRTCAudioManager;
41
42public class WebRTCWrapper {
43
44 private VideoTrack localVideoTrack = null;
45 private VideoTrack remoteVideoTrack = null;
46
47 private final EventCallback eventCallback;
48
49 private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() {
50 @Override
51 public void onSignalingChange(PeerConnection.SignalingState signalingState) {
52 Log.d(Config.LOGTAG, "onSignalingChange(" + signalingState + ")");
53 //this is called after removeTrack or addTrack
54 //and should then trigger a content-add or content-remove or something
55 //https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack
56 }
57
58 @Override
59 public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
60 eventCallback.onConnectionChange(newState);
61 }
62
63 @Override
64 public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
65
66 }
67
68 @Override
69 public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) {
70 Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote);
71 Log.d(Config.LOGTAG, "local candidate selected: " + event.local);
72 }
73
74 @Override
75 public void onIceConnectionReceivingChange(boolean b) {
76
77 }
78
79 @Override
80 public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
81
82 }
83
84 @Override
85 public void onIceCandidate(IceCandidate iceCandidate) {
86 eventCallback.onIceCandidate(iceCandidate);
87 }
88
89 @Override
90 public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
91
92 }
93
94 @Override
95 public void onAddStream(MediaStream mediaStream) {
96 Log.d(Config.LOGTAG, "onAddStream");
97 for (AudioTrack audioTrack : mediaStream.audioTracks) {
98 Log.d(Config.LOGTAG, "remote? - audioTrack enabled:" + audioTrack.enabled() + " state=" + audioTrack.state());
99 }
100 final List<VideoTrack> videoTracks = mediaStream.videoTracks;
101 if (videoTracks.size() > 0) {
102 Log.d(Config.LOGTAG, "more than zero remote video tracks found. using first");
103 remoteVideoTrack = videoTracks.get(0);
104 }
105 }
106
107 @Override
108 public void onRemoveStream(MediaStream mediaStream) {
109
110 }
111
112 @Override
113 public void onDataChannel(DataChannel dataChannel) {
114
115 }
116
117 @Override
118 public void onRenegotiationNeeded() {
119
120 }
121
122 @Override
123 public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
124 Log.d(Config.LOGTAG, "onAddTrack()");
125
126 }
127 };
128 private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() {
129 @Override
130 public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
131 eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
132 }
133 };
134 @Nullable
135 private PeerConnection peerConnection = null;
136 private AudioTrack localAudioTrack = null;
137 private AppRTCAudioManager appRTCAudioManager = null;
138 private final Handler mainHandler = new Handler(Looper.getMainLooper());
139
140 public WebRTCWrapper(final EventCallback eventCallback) {
141 this.eventCallback = eventCallback;
142 }
143
144 public void setup(final Context context) {
145 PeerConnectionFactory.initialize(
146 PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()
147 );
148 mainHandler.post(() -> {
149 appRTCAudioManager = AppRTCAudioManager.create(context, AppRTCAudioManager.SpeakerPhonePreference.EARPIECE);
150 appRTCAudioManager.start(audioManagerEvents);
151 eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices());
152 });
153 }
154
155 public void initializePeerConnection(final List<PeerConnection.IceServer> iceServers) throws InitializationException {
156 PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
157
158 CameraVideoCapturer capturer = null;
159 Camera1Enumerator camera1Enumerator = new Camera1Enumerator();
160 for (String deviceName : camera1Enumerator.getDeviceNames()) {
161 Log.d(Config.LOGTAG, "camera device name: " + deviceName);
162 if (camera1Enumerator.isFrontFacing(deviceName)) {
163 capturer = camera1Enumerator.createCapturer(deviceName, new CameraVideoCapturer.CameraEventsHandler() {
164 @Override
165 public void onCameraError(String s) {
166
167 }
168
169 @Override
170 public void onCameraDisconnected() {
171
172 }
173
174 @Override
175 public void onCameraFreezed(String s) {
176
177 }
178
179 @Override
180 public void onCameraOpening(String s) {
181 Log.d(Config.LOGTAG, "onCameraOpening");
182 }
183
184 @Override
185 public void onFirstFrameAvailable() {
186 Log.d(Config.LOGTAG, "onFirstFrameAvailable");
187 }
188
189 @Override
190 public void onCameraClosed() {
191
192 }
193 });
194 }
195 }
196
197 /*if (capturer != null) {
198 capturer.initialize();
199 Log.d(Config.LOGTAG,"start capturing");
200 capturer.startCapture(800,600,30);
201 }*/
202
203 final VideoSource videoSource = peerConnectionFactory.createVideoSource(false);
204 final VideoTrack videoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource);
205
206 final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
207
208 this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource);
209 final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream");
210 stream.addTrack(this.localAudioTrack);
211 //stream.addTrack(videoTrack);
212
213 this.localVideoTrack = videoTrack;
214
215 final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver);
216 if (peerConnection == null) {
217 throw new InitializationException("Unable to create PeerConnection");
218 }
219 peerConnection.addStream(stream);
220 peerConnection.setAudioPlayout(true);
221 peerConnection.setAudioRecording(true);
222 this.peerConnection = peerConnection;
223 }
224
225 public void close() {
226 final PeerConnection peerConnection = this.peerConnection;
227 if (peerConnection != null) {
228 peerConnection.close();
229 }
230 final AppRTCAudioManager audioManager = this.appRTCAudioManager;
231 if (audioManager != null) {
232 mainHandler.post(audioManager::stop);
233 }
234 }
235
236 public void setMicrophoneEnabled(final boolean enabled) {
237 final AudioTrack audioTrack = this.localAudioTrack;
238 if (audioTrack == null) {
239 throw new IllegalStateException("Local audio track does not exist (yet)");
240 }
241 audioTrack.setEnabled(enabled);
242 }
243
244 public boolean isMicrophoneEnabled() {
245 final AudioTrack audioTrack = this.localAudioTrack;
246 if (audioTrack == null) {
247 throw new IllegalStateException("Local audio track does not exist (yet)");
248 }
249 return audioTrack.enabled();
250 }
251
252
253 public ListenableFuture<SessionDescription> createOffer() {
254 return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
255 final SettableFuture<SessionDescription> future = SettableFuture.create();
256 peerConnection.createOffer(new CreateSdpObserver() {
257 @Override
258 public void onCreateSuccess(SessionDescription sessionDescription) {
259 future.set(sessionDescription);
260 }
261
262 @Override
263 public void onCreateFailure(String s) {
264 future.setException(new IllegalStateException("Unable to create offer: " + s));
265 }
266 }, new MediaConstraints());
267 return future;
268 }, MoreExecutors.directExecutor());
269 }
270
271 public ListenableFuture<SessionDescription> createAnswer() {
272 return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
273 final SettableFuture<SessionDescription> future = SettableFuture.create();
274 peerConnection.createAnswer(new CreateSdpObserver() {
275 @Override
276 public void onCreateSuccess(SessionDescription sessionDescription) {
277 future.set(sessionDescription);
278 }
279
280 @Override
281 public void onCreateFailure(String s) {
282 future.setException(new IllegalStateException("Unable to create answer: " + s));
283 }
284 }, new MediaConstraints());
285 return future;
286 }, MoreExecutors.directExecutor());
287 }
288
289 public ListenableFuture<Void> setLocalDescription(final SessionDescription sessionDescription) {
290 return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
291 final SettableFuture<Void> future = SettableFuture.create();
292 peerConnection.setLocalDescription(new SetSdpObserver() {
293 @Override
294 public void onSetSuccess() {
295 future.set(null);
296 }
297
298 @Override
299 public void onSetFailure(String s) {
300 future.setException(new IllegalArgumentException("unable to set local session description: " + s));
301
302 }
303 }, sessionDescription);
304 return future;
305 }, MoreExecutors.directExecutor());
306 }
307
308 public ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) {
309 return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
310 final SettableFuture<Void> future = SettableFuture.create();
311 peerConnection.setRemoteDescription(new SetSdpObserver() {
312 @Override
313 public void onSetSuccess() {
314 future.set(null);
315 }
316
317 @Override
318 public void onSetFailure(String s) {
319 future.setException(new IllegalArgumentException("unable to set remote session description: " + s));
320
321 }
322 }, sessionDescription);
323 return future;
324 }, MoreExecutors.directExecutor());
325 }
326
327 @Nonnull
328 private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
329 final PeerConnection peerConnection = this.peerConnection;
330 if (peerConnection == null) {
331 return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first"));
332 } else {
333 return Futures.immediateFuture(peerConnection);
334 }
335 }
336
337 public void addIceCandidate(IceCandidate iceCandidate) {
338 requirePeerConnection().addIceCandidate(iceCandidate);
339 }
340
341 public PeerConnection.PeerConnectionState getState() {
342 return requirePeerConnection().connectionState();
343 }
344
345 private PeerConnection requirePeerConnection() {
346 final PeerConnection peerConnection = this.peerConnection;
347 if (peerConnection == null) {
348 throw new IllegalStateException("initialize PeerConnection first");
349 }
350 return peerConnection;
351 }
352
353 public AppRTCAudioManager getAudioManager() {
354 return appRTCAudioManager;
355 }
356
357 private static abstract class SetSdpObserver implements SdpObserver {
358
359 @Override
360 public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
361 throw new IllegalStateException("Not able to use SetSdpObserver");
362 }
363
364 @Override
365 public void onCreateFailure(String s) {
366 throw new IllegalStateException("Not able to use SetSdpObserver");
367 }
368
369 }
370
371 private static abstract class CreateSdpObserver implements SdpObserver {
372
373
374 @Override
375 public void onSetSuccess() {
376 throw new IllegalStateException("Not able to use CreateSdpObserver");
377 }
378
379
380 @Override
381 public void onSetFailure(String s) {
382 throw new IllegalStateException("Not able to use CreateSdpObserver");
383 }
384 }
385
386 public static class InitializationException extends Exception {
387
388 private InitializationException(String message) {
389 super(message);
390 }
391 }
392
393 public interface EventCallback {
394 void onIceCandidate(IceCandidate iceCandidate);
395
396 void onConnectionChange(PeerConnection.PeerConnectionState newState);
397
398 void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
399 }
400}