JingleRtpConnection.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.util.Log;
  4
  5import com.google.common.collect.ImmutableList;
  6import com.google.common.collect.ImmutableMap;
  7
  8import org.webrtc.AudioSource;
  9import org.webrtc.AudioTrack;
 10import org.webrtc.DataChannel;
 11import org.webrtc.IceCandidate;
 12import org.webrtc.MediaConstraints;
 13import org.webrtc.MediaStream;
 14import org.webrtc.PeerConnection;
 15import org.webrtc.PeerConnectionFactory;
 16import org.webrtc.RtpReceiver;
 17import org.webrtc.SdpObserver;
 18
 19import java.util.Collection;
 20import java.util.Collections;
 21import java.util.Map;
 22
 23import eu.siacs.conversations.Config;
 24import eu.siacs.conversations.xml.Element;
 25import eu.siacs.conversations.xml.Namespace;
 26import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 27import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 28import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 29import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 30import rocks.xmpp.addr.Jid;
 31
 32public class JingleRtpConnection extends AbstractJingleConnection {
 33
 34    private static final Map<State, Collection<State>> VALID_TRANSITIONS;
 35
 36    static {
 37        final ImmutableMap.Builder<State, Collection<State>> transitionBuilder = new ImmutableMap.Builder<>();
 38        transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED));
 39        transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED));
 40        transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED));
 41        VALID_TRANSITIONS = transitionBuilder.build();
 42    }
 43
 44    private State state = State.NULL;
 45    private RtpContentMap initialRtpContentMap;
 46    private PeerConnection peerConnection;
 47
 48
 49    public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
 50        super(jingleConnectionManager, id, initiator);
 51    }
 52
 53    @Override
 54    void deliverPacket(final JinglePacket jinglePacket) {
 55        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection");
 56        switch (jinglePacket.getAction()) {
 57            case SESSION_INITIATE:
 58                receiveSessionInitiate(jinglePacket);
 59                break;
 60            default:
 61                Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction()));
 62                break;
 63        }
 64    }
 65
 66    private void receiveSessionInitiate(final JinglePacket jinglePacket) {
 67        Log.d(Config.LOGTAG,jinglePacket.toString());
 68        if (isInitiator()) {
 69            Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid()));
 70            //TODO respond with out-of-order
 71            return;
 72        }
 73        final RtpContentMap contentMap;
 74        try {
 75            contentMap = RtpContentMap.of(jinglePacket);
 76        } catch (IllegalArgumentException | NullPointerException e) {
 77            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
 78            return;
 79        }
 80        Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents");
 81        final State oldState = this.state;
 82        if (transition(State.SESSION_INITIALIZED)) {
 83            if (oldState == State.PROCEED) {
 84                processContents(contentMap);
 85                sendSessionAccept();
 86            } else {
 87                //TODO start ringing
 88            }
 89        } else {
 90            Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state));
 91        }
 92    }
 93
 94    private void processContents(final RtpContentMap contentMap) {
 95        setupWebRTC();
 96        org.webrtc.SessionDescription sessionDescription = new org.webrtc.SessionDescription(org.webrtc.SessionDescription.Type.OFFER, SessionDescription.of(contentMap).toString());
 97        Log.d(Config.LOGTAG, "debug print for sessionDescription:" + sessionDescription.description);
 98        this.peerConnection.setRemoteDescription(new SdpObserver() {
 99            @Override
100            public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
101
102            }
103
104            @Override
105            public void onSetSuccess() {
106                Log.d(Config.LOGTAG, "onSetSuccess() for setRemoteDescription");
107            }
108
109            @Override
110            public void onCreateFailure(String s) {
111
112            }
113
114            @Override
115            public void onSetFailure(String s) {
116                Log.d(Config.LOGTAG, "onSetFailure() for setRemoteDescription. " + s);
117
118            }
119        }, sessionDescription);
120    }
121
122    void deliveryMessage(final Jid from, final Element message) {
123        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message);
124        switch (message.getName()) {
125            case "propose":
126                receivePropose(from, message);
127                break;
128            case "proceed":
129                receiveProceed(from, message);
130            default:
131                break;
132        }
133    }
134
135    private void receivePropose(final Jid from, final Element propose) {
136        final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid());
137        if (originatedFromMyself) {
138            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring");
139        } else if (transition(State.PROPOSED)) {
140            //TODO start ringing or something
141            pickUpCall();
142        } else {
143            Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state);
144        }
145    }
146
147    private void receiveProceed(final Jid from, final Element proceed) {
148        if (from.equals(id.with)) {
149            if (isInitiator()) {
150                if (transition(State.PROCEED)) {
151                    this.sendSessionInitiate();
152                } else {
153                    Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state));
154                }
155            } else {
156                Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid()));
157            }
158        } else {
159            Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with));
160        }
161    }
162
163    private void sendSessionInitiate() {
164        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
165        setupWebRTC();
166        createOffer();
167    }
168
169    private void sendSessionInitiate(RtpContentMap rtpContentMap) {
170        this.initialRtpContentMap = rtpContentMap;
171        final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
172        Log.d(Config.LOGTAG, sessionInitiate.toString());
173        Log.d(Config.LOGTAG, "here is what we think the sdp looks like" + SessionDescription.of(rtpContentMap).toString());
174        send(sessionInitiate);
175    }
176
177    private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) {
178        final RtpContentMap transportInfo;
179        try {
180            transportInfo = this.initialRtpContentMap.transportInfo(contentName, candidate);
181        } catch (Exception e) {
182            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName);
183            return;
184        }
185        final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
186        Log.d(Config.LOGTAG, jinglePacket.toString());
187        send(jinglePacket);
188    }
189
190    private void send(final JinglePacket jinglePacket) {
191        jinglePacket.setTo(id.with);
192        //TODO track errors
193        xmppConnectionService.sendIqPacket(id.account, jinglePacket, null);
194    }
195
196
197    private void sendSessionAccept() {
198        Log.d(Config.LOGTAG, "sending session-accept");
199    }
200
201    public void pickUpCall() {
202        switch (this.state) {
203            case PROPOSED:
204                pickupCallFromProposed();
205                break;
206            case SESSION_INITIALIZED:
207                pickupCallFromSessionInitialized();
208                break;
209            default:
210                throw new IllegalStateException("Can not pick up call from " + this.state);
211        }
212    }
213
214    private void setupWebRTC() {
215        PeerConnectionFactory.initialize(
216                PeerConnectionFactory.InitializationOptions.builder(xmppConnectionService).createInitializationOptions()
217        );
218        final PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
219        PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
220
221        final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
222
223        final AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource);
224        final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream");
225        stream.addTrack(audioTrack);
226
227
228        this.peerConnection = peerConnectionFactory.createPeerConnection(Collections.emptyList(), new PeerConnection.Observer() {
229            @Override
230            public void onSignalingChange(PeerConnection.SignalingState signalingState) {
231
232            }
233
234            @Override
235            public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
236
237            }
238
239            @Override
240            public void onIceConnectionReceivingChange(boolean b) {
241
242            }
243
244            @Override
245            public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
246                Log.d(Config.LOGTAG, "onIceGatheringChange() " + iceGatheringState);
247            }
248
249            @Override
250            public void onIceCandidate(IceCandidate iceCandidate) {
251                IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp);
252                Log.d(Config.LOGTAG, "onIceCandidate: " + iceCandidate.sdp);
253                sendTransportInfo(iceCandidate.sdpMid, candidate);
254
255            }
256
257            @Override
258            public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
259
260            }
261
262            @Override
263            public void onAddStream(MediaStream mediaStream) {
264
265            }
266
267            @Override
268            public void onRemoveStream(MediaStream mediaStream) {
269
270            }
271
272            @Override
273            public void onDataChannel(DataChannel dataChannel) {
274
275            }
276
277            @Override
278            public void onRenegotiationNeeded() {
279
280            }
281
282            @Override
283            public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
284
285            }
286        });
287
288        peerConnection.addStream(stream);
289    }
290
291    private void createOffer() {
292        Log.d(Config.LOGTAG, "createOffer()");
293        peerConnection.createOffer(new SdpObserver() {
294
295            @Override
296            public void onCreateSuccess(org.webrtc.SessionDescription description) {
297                final SessionDescription sessionDescription = SessionDescription.parse(description.description);
298                Log.d(Config.LOGTAG, "description: " + description.description);
299                final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
300                sendSessionInitiate(rtpContentMap);
301                peerConnection.setLocalDescription(new SdpObserver() {
302                    @Override
303                    public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
304
305                    }
306
307                    @Override
308                    public void onSetSuccess() {
309                        Log.d(Config.LOGTAG, "onSetSuccess()");
310                    }
311
312                    @Override
313                    public void onCreateFailure(String s) {
314
315                    }
316
317                    @Override
318                    public void onSetFailure(String s) {
319
320                    }
321                }, description);
322            }
323
324            @Override
325            public void onSetSuccess() {
326
327            }
328
329            @Override
330            public void onCreateFailure(String s) {
331
332            }
333
334            @Override
335            public void onSetFailure(String s) {
336
337            }
338        }, new MediaConstraints());
339    }
340
341    private void pickupCallFromProposed() {
342        transitionOrThrow(State.PROCEED);
343        final MessagePacket messagePacket = new MessagePacket();
344        messagePacket.setTo(id.with);
345        //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916
346        messagePacket.addChild("proceed", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId);
347        Log.d(Config.LOGTAG, messagePacket.toString());
348        xmppConnectionService.sendMessagePacket(id.account, messagePacket);
349    }
350
351    private void pickupCallFromSessionInitialized() {
352
353    }
354
355    private synchronized boolean transition(final State target) {
356        final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
357        if (validTransitions != null && validTransitions.contains(target)) {
358            this.state = target;
359            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
360            return true;
361        } else {
362            return false;
363        }
364    }
365
366    public void transitionOrThrow(final State target) {
367        if (!transition(target)) {
368            throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target));
369        }
370    }
371}