JingleRtpConnection.java

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