WebRTCWrapper.java

  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}