Rewrite codec querying
[emacs-jabber-tox.git] / jabber-tox.el
blobee695aaa1af762bf511dba6810c5765107ffcd69
1 ;; jabber-tox.el - Jingle support using TOX
3 ;; Copyright (C) 2008 - Magnus Henoch - mange@freemail.hu
5 ;; This file is (soon) a part of jabber.el.
7 ;; This program is free software; you can redistribute it and/or modify
8 ;; it under the terms of the GNU General Public License as published by
9 ;; the Free Software Foundation; either version 2 of the License, or
10 ;; (at your option) any later version.
12 ;; This program is distributed in the hope that it will be useful,
13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ;; GNU General Public License for more details.
17 ;; You should have received a copy of the GNU General Public License
18 ;; along with this program; if not, write to the Free Software
19 ;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 (require 'dbus)
22 (require 'fsm)
23 (require 'cl)
25 (defconst jingle-ns "urn:xmpp:jingle:0"
26 "Jingle namespace (XEP-0166)")
28 (defconst jingle-error-ns "urn:xmpp:jingle:errors:0"
29 "Jingle error namespace (XEP-0166)")
31 (defconst jingle-rtp-ns "urn:xmpp:jingle:apps:rtp:0"
32 "Jingle RTP Sessions namespace (XEP-0167)")
34 (defconst jingle-ice-udp-ns "urn:xmpp:jingle:transports:ice-udp:0"
35 "Jingle ICE namespace (XEP-0176)")
37 (defvar jingle-acct-sid-map (make-hash-table :test 'equal)
38 "Mapping from pairs of JIDs and Jingle SIDs to FSMs.
39 The JID is the full JID of the account using the session.")
41 (defconst tox-name "net.sourceforge.emacs-jabber.Tox"
42 "Well-known D-BUS name of the tox service.")
44 (defconst tox-path "/net/sourceforge/emacs_jabber/Tox"
45 "Well-known path of the main Tox object.")
47 (defconst tox-interface "net.sourceforge.emacs_jabber.Tox"
48 "Interface of main Tox object.")
50 (defconst tox-session-interface "net.sourceforge.emacs_jabber.ToxSession"
51 "Interface of ToxSession object.")
53 (define-fsm jingle
54 :start ((jc sid role jid) "Start a Jingle FSM.
55 \(Specifically, for Jingle Audio, as that's all we support for now.)
56 JC is the account we're using.
57 SID is a string, the session ID.
58 ROLE is either :initiator or :target.
59 JID is the full JID of the partner."
60 (let ((state-data (list :jc jc :jid jid :sid sid :role role)))
61 (setq state-data (jingle-create-tox-session fsm state-data))
62 (list
63 (ecase (plist-get state-data :role)
64 (:initiator
65 :initiate)
66 (:target
67 :wait-for-initiate))
68 state-data))))
70 (defun jingle-create-tox-session (fsm state-data)
71 "Helper function to create a Tox session.
72 Accepts, modifies, and returns STATE-DATA."
73 ;; XXX: should this always be bidirectional?
74 (let* ((tox-session
75 (dbus-call-method :session tox-name tox-path tox-interface
76 "CreateSession"
77 :byte 3)) ;3=bidirectional stream
78 ;; Find the codecs that we support
79 (our-codecs
80 (dbus-call-method :session tox-name tox-session tox-session-interface
81 "GetLocalCodecs")))
82 (setq state-data (plist-put state-data :tox-session tox-session))
83 (setq state-data (plist-put state-data :our-codecs our-codecs))
85 (fsm-debug-output "tox-session: %S, our-codecs: %S" tox-session our-codecs)
86 ;; Set up the pipeline, so we can search for transport candidates.
87 (fsm-debug-output "About to call SetDefaultAudioSink")
88 (dbus-call-method :session tox-name tox-session tox-session-interface
89 "SetDefaultAudioSink")
90 (fsm-debug-output "About to call SetOggVorbisAudioSource")
91 (dbus-call-method :session tox-name tox-session tox-session-interface
92 "SetOggVorbisAudioSource"
93 "/home/magnus/musik/computervirus.ogg")
94 ;; There, now we just wait for the NativeCandidatesPrepared signal...
95 ;; NO! Don't do like those dead people do! That signal will never
96 ;; come. I don't know why, though...
97 (fsm-debug-output "About to register signal")
98 (dbus-register-signal :session tox-name tox-session tox-session-interface
99 "NativeCandidatesPrepared"
100 (lexical-let ((fsm fsm))
101 (lambda (components)
102 (fsm-send-sync
103 fsm
104 (cons :native-candidates-prepared components)))))
105 ;; This is more like it. At least it will provide us with some
106 ;; debugging information.
107 (dbus-register-signal :session tox-name tox-session tox-session-interface
108 "NewNativeCandidate"
109 (lexical-let ((fsm fsm))
110 (lambda (components)
111 (fsm-send-sync
112 fsm
113 (cons :new-native-candidate components)))))
114 (fsm-debug-output "Waiting")
115 state-data))
117 (define-enter-state jingle nil
118 (fsm state-data)
119 ;; clean up
120 (let ((tox-session (plist-get state-data :tox-session)))
121 (when tox-session
122 (ignore-errors
123 (dbus-call-method :session tox-name tox-session tox-session-interface
124 "Destroy"))))
125 (remhash (cons (jabber-connection-jid (plist-get state-data :jc))
126 (plist-get state-data :sid))
127 jingle-acct-sid-map)
128 (list nil nil))
130 (define-enter-state jingle :initiate
131 (fsm state-data)
132 (let ((jc (plist-get state-data :jc))
133 (jid (plist-get state-data :jid))
134 (sid (plist-get state-data :sid))
135 (payload-types (mapcar
136 (lambda (codec)
137 `(payload-type
138 ((id . ,(number-to-string (nth 0 codec)))
139 (name . ,(nth 1 codec))
140 ;; (nth 2 codec) is media type;
141 ;; should we filter out
142 ;; non-audio codecs? then
143 ;; again, the value seems to be
144 ;; bogus...
145 (clockrate . ,(number-to-string (nth 3 codec)))
146 ,@(let ((channels (nth 4 codec)))
147 (unless (= channels 0)
148 `((channels . ,(number-to-string channels))))))
149 ,@(mapcar
150 (lambda (param)
151 `(parameter
152 ((name . ,(nth 0 param))
153 (value . ,(nth 1 param)))))
154 (nth 5 codec))))
155 (plist-get state-data :our-codecs))))
156 (jabber-send-iq jc jid "set"
157 `(jingle ((xmlns . ,jingle-ns)
158 (action . "session-initiate")
159 (initiator . ,(jabber-connection-jid jc))
160 (sid . ,sid))
161 (content
162 ((creator . "initiator")
163 (name . "foo")
164 (senders . "initiator"))
165 (description
166 ((xmlns . ,jingle-rtp-ns)
167 (media . "audio"))
168 ,@payload-types)
169 (transport ((xmlns . ,jingle-ice-udp-ns)))))
170 (lambda (jc iq fsm)
171 (fsm-send-sync fsm (cons :iq-result iq)))
173 (lambda (jc iq fsm)
174 (fsm-send-sync fsm (cons :iq-error iq)))
175 fsm)
176 (list state-data nil)))
178 (define-state jingle :initiate
179 (fsm state-data event callback)
180 (case (car-safe event)
181 (:iq-result
182 ;; Receiver provisionally accepted the session request. Move on
183 ;; to PENDING.
184 (list :pending state-data))
185 (:iq-error
186 (message "Couldn't initiate Jingle audio session: %s"
187 (jabber-parse-error (jabber-iq-error (cdr event))))
188 (list nil state-data))
189 (:new-native-candidate
190 (let ((components (cdr event)))
191 ;; XXX: keep them all
192 (setq state-data (plist-put state-data :native-candidates (list (cdr event))))
193 (jingle-send-native-candidate state-data components)
194 (list :initiate state-data)))))
196 (define-state jingle :wait-for-initiate
197 (fsm state-data event callback)
198 (case (car-safe event)
199 (:iq-set
200 (let* ((jc (plist-get state-data :jc))
201 (iq (cdr event))
202 (from (jabber-xml-get-attribute iq 'from))
203 (id (jabber-xml-get-attribute iq 'id))
204 (jingle (jabber-iq-query iq))
205 ;; XXX: could be more than one...
206 (content (car (jabber-xml-get-children jingle 'content)))
207 ;; XXX: is it really audio?
208 (audio-content (find jingle-rtp-ns (jabber-xml-node-children content)
209 :test 'string=
210 :key 'jabber-xml-get-xmlns))
211 (payload-types (jabber-xml-get-children audio-content 'payload-type)))
212 ;; There are very few reasons for which we should not send an
213 ;; acknowledgement here; see section 6.3.2 of XEP-0166.
214 ;; Notably, we might want to check that there is a presence
215 ;; subscription.
216 (jabber-send-iq jc from "result" ()
217 nil nil nil nil id)
219 (cond
220 ;; Make sure audio is in the list of contents. We can
221 ;; negotiate away other content types later.
222 ((null audio-content)
223 (jingle-send-iq state-data "session-terminate"
224 '(reason () (unsupported-applications)))
225 (list nil state-data))
227 ;; Make sure ICE is in the list of transports.
228 ((not (member* jingle-ice-udp-ns
229 (jabber-xml-get-children content 'transport)
230 :test 'string=
231 :key 'jabber-xml-get-xmlns))
232 (jingle-send-iq state-data "session-terminate"
233 '(reason () (unsupported-transports)))
234 (list nil state-data))
237 (let ((tox-session (plist-get state-data :tox-session)))
238 ;; Tell tox what codecs the remote side supports
239 (dbus-call-method
240 :session tox-name tox-session tox-session-interface
241 "SetRemoteCodecs"
242 ;;'((array (struct int32 string byte uint32 uint32 (array (dict-entry string string)))))
243 (mapcar
244 (lambda (pt)
245 (jabber-xml-let-attributes
246 (id name clockrate channels) pt
247 (list :struct
248 :int32 (string-to-number id)
249 :string name
250 :byte 0
251 :uint32 clockrate
252 :uint32 channels
253 (cons
254 :array
256 (mapcar
257 (lambda (param)
258 (jabber-xml-let-attributes
259 (name value) param
260 (list :dict-entry :string name :string value)))
261 (jabber-xml-get-children pt 'parameter))
262 (list :signature "{ss}"))))))
263 payload-types))
265 ;; Check if we have any codecs in common
266 (let ((codec-intersection
267 (dbus-call-method
268 :session tox-name tox-session tox-session-interface
269 "GetCodecIntersection")))
270 (setq state-data
271 (plist-put
272 state-data
273 :codec-intersection codec-intersection))
275 (if codec-intersection
276 ;; So, now we know that we stand a basic chance of fulfilling
277 ;; the request. Let's move on to PENDING.
278 (list :pending state-data)
280 ;; Or, it might turn out that we don't have any codecs
281 ;; in common with our partner.
282 (jingle-send-iq state-data "session-terminate"
283 '(reason () (media-error)))
284 (list nil state-data))))))))))
286 (define-state jingle :wait-for-codec-intersection
287 (fsm state-data event callback)
289 ;; ;; Good enough, provisionally accept.
290 ;; (jabber-send-iq jc from "result" ()
291 ;; nil nil nil nil id)
292 ;; (list :pending state-data)))))))
296 (defun jingle-send-iq (state-data action payload)
297 "Send a Jingle IQ stanza from within a Jingle FSM.
298 STATE-DATA is the state data plist of the FSM.
299 ACTION is the value of the action attribute of the <jingle/>
300 element.
301 PAYLOAD is a list of XML elements to include as children
302 of the <jingle/> element.
303 The recipient and the SID are determined from STATE-DATA."
304 (let ((jc (plist-get state-data :jc))
305 (jid (plist-get state-data :jid))
306 (role (plist-get state-data :role))
307 (sid (plist-get state-data :sid)))
308 (jabber-send-iq
309 jc jid "set"
310 `(jingle ((xmlns . ,jingle-ns)
311 (action . ,action)
312 (initiator
313 . ,(ecase role
314 (:initiator
315 (jabber-connection-jid jc))
316 (:target
317 jid)))
318 (sid . ,sid))
319 ,@payload)
320 ;; XXX: we probably want error checking, to see if our partner
321 ;; went offline.
322 nil nil nil nil)))
324 (defun jingle-send-native-candidate (state-data candidate)
325 "Send a native candidate for ICE-UDP.
326 The CANDIDATE is a list of components, as provided by the
327 NewNativeCandidate signal of Tox."
328 (jingle-send-iq state-data "transport-info"
329 `(content
330 ((creator . "initiator")
331 (name . "foo"))
332 (transport
333 ((xmlns . ,jingle-ice-udp-ns))
334 ,@(mapcar
335 (lambda (c)
336 `(candidate
337 ((component . ,(number-to-string (nth 1 c)))
338 ;; foundation?
339 ;; generation?
340 (ip . ,(nth 2 c))
341 ;; network?
342 (port . ,(number-to-string (nth 3 c)))
343 (protocol . ,(nth 4 c))
344 (priority . ,(nth 7 c))
345 ;; how to translate type?
347 candidate)))))
349 (add-to-list 'jabber-iq-set-xmlns-alist
350 (cons jingle-ns 'jabber-jingle-incoming-iq))
351 (defun jabber-jingle-incoming-iq (jc iq)
352 (jabber-xml-let-attributes
353 (sid action) (jabber-iq-query iq)
354 (unless (and sid action)
355 (jabber-signal-error "modify" 'bad-request))
356 (let ((fsm (gethash (cons (jabber-connection-jid jc) sid) jingle-acct-sid-map)))
357 (cond
358 (fsm
359 (fsm-send-sync fsm (cons :iq-set iq)))
360 ((string= action "session-initiate")
361 (condition-case e
362 (setq fsm (start-jingle jc sid :target (jabber-xml-get-attribute iq 'from)))
363 (error
364 (jabber-signal-error "wait" 'internal-server-error
365 (concat "Couldn't accept Jingle session: "
366 (error-message-string e)))))
367 (puthash (cons (jabber-connection-jid jc) sid) fsm jingle-acct-sid-map)
368 (fsm-send-sync fsm (cons :iq-set iq)))
370 (jabber-signal-error "modify" 'bad-request
371 (format "Session \"%s\" unknown" sid)
372 `((unknown-session ((xmlns . ,jingle-error-ns))))))))))
374 (defun jabber-jingle-start-audio-session (jc jid)
375 (interactive
376 (list (jabber-read-account)
377 (jabber-read-jid-completing "Voice call to: " nil nil nil 'full)))
378 (let* ((sid (apply 'format "emacs-sid-%d.%d.%d" (current-time)))
379 (fsm (start-jingle jc sid :initiator jid)))
380 (puthash (cons (jabber-connection-jid jc) sid) fsm jingle-acct-sid-map)))
383 (provide 'jabber-tox)