1
0

jitsimeetbridge.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. #!/usr/bin/env python
  2. """
  3. This is an attempt at bridging matrix clients into a Jitis meet room via Matrix
  4. video call. It uses hard-coded xml strings overg XMPP BOSH. It can display one
  5. of the streams from the Jitsi bridge until the second lot of SDP comes down and
  6. we set the remote SDP at which point the stream ends. Our video never gets to
  7. the bridge.
  8. Requires:
  9. npm install jquery jsdom
  10. """
  11. import gevent
  12. import grequests
  13. from BeautifulSoup import BeautifulSoup
  14. import json
  15. import urllib
  16. import subprocess
  17. import time
  18. #ACCESS_TOKEN="" #
  19. MATRIXBASE = 'https://matrix.org/_matrix/client/api/v1/'
  20. MYUSERNAME = '@davetest:matrix.org'
  21. HTTPBIND = 'https://meet.jit.si/http-bind'
  22. #HTTPBIND = 'https://jitsi.vuc.me/http-bind'
  23. #ROOMNAME = "matrix"
  24. ROOMNAME = "pibble"
  25. HOST="guest.jit.si"
  26. #HOST="jitsi.vuc.me"
  27. TURNSERVER="turn.guest.jit.si"
  28. #TURNSERVER="turn.jitsi.vuc.me"
  29. ROOMDOMAIN="meet.jit.si"
  30. #ROOMDOMAIN="conference.jitsi.vuc.me"
  31. class TrivialMatrixClient:
  32. def __init__(self, access_token):
  33. self.token = None
  34. self.access_token = access_token
  35. def getEvent(self):
  36. while True:
  37. url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
  38. if self.token:
  39. url += "&from="+self.token
  40. req = grequests.get(url)
  41. resps = grequests.map([req])
  42. obj = json.loads(resps[0].content)
  43. print "incoming from matrix",obj
  44. if 'end' not in obj:
  45. continue
  46. self.token = obj['end']
  47. if len(obj['chunk']):
  48. return obj['chunk'][0]
  49. def joinRoom(self, roomId):
  50. url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
  51. print url
  52. headers={ 'Content-Type': 'application/json' }
  53. req = grequests.post(url, headers=headers, data='{}')
  54. resps = grequests.map([req])
  55. obj = json.loads(resps[0].content)
  56. print "response: ",obj
  57. def sendEvent(self, roomId, evType, event):
  58. url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
  59. print url
  60. print json.dumps(event)
  61. headers={ 'Content-Type': 'application/json' }
  62. req = grequests.post(url, headers=headers, data=json.dumps(event))
  63. resps = grequests.map([req])
  64. obj = json.loads(resps[0].content)
  65. print "response: ",obj
  66. xmppClients = {}
  67. def matrixLoop():
  68. while True:
  69. ev = matrixCli.getEvent()
  70. print ev
  71. if ev['type'] == 'm.room.member':
  72. print 'membership event'
  73. if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
  74. roomId = ev['room_id']
  75. print "joining room %s" % (roomId)
  76. matrixCli.joinRoom(roomId)
  77. elif ev['type'] == 'm.room.message':
  78. if ev['room_id'] in xmppClients:
  79. print "already have a bridge for that user, ignoring"
  80. continue
  81. print "got message, connecting"
  82. xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
  83. gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
  84. elif ev['type'] == 'm.call.invite':
  85. print "Incoming call"
  86. #sdp = ev['content']['offer']['sdp']
  87. #print "sdp: %s" % (sdp)
  88. #xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
  89. #gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
  90. elif ev['type'] == 'm.call.answer':
  91. print "Call answered"
  92. sdp = ev['content']['answer']['sdp']
  93. if ev['room_id'] not in xmppClients:
  94. print "We didn't have a call for that room"
  95. continue
  96. # should probably check call ID too
  97. xmppCli = xmppClients[ev['room_id']]
  98. xmppCli.sendAnswer(sdp)
  99. elif ev['type'] == 'm.call.hangup':
  100. if ev['room_id'] in xmppClients:
  101. xmppClients[ev['room_id']].stop()
  102. del xmppClients[ev['room_id']]
  103. class TrivialXmppClient:
  104. def __init__(self, matrixRoom, userId):
  105. self.rid = 0
  106. self.matrixRoom = matrixRoom
  107. self.userId = userId
  108. self.running = True
  109. def stop(self):
  110. self.running = False
  111. def nextRid(self):
  112. self.rid += 1
  113. return '%d' % (self.rid)
  114. def sendIq(self, xml):
  115. fullXml = "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s'>%s</body>" % (self.nextRid(), self.sid, xml)
  116. #print "\t>>>%s" % (fullXml)
  117. return self.xmppPoke(fullXml)
  118. def xmppPoke(self, xml):
  119. headers = {'Content-Type': 'application/xml'}
  120. req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
  121. resps = grequests.map([req])
  122. obj = BeautifulSoup(resps[0].content)
  123. return obj
  124. def sendAnswer(self, answer):
  125. print "sdp from matrix client",answer
  126. p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--sdp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  127. jingle, out_err = p.communicate(answer)
  128. jingle = jingle % {
  129. 'tojid': self.callfrom,
  130. 'action': 'session-accept',
  131. 'initiator': self.callfrom,
  132. 'responder': self.jid,
  133. 'sid': self.callsid
  134. }
  135. print "answer jingle from sdp",jingle
  136. res = self.sendIq(jingle)
  137. print "reply from answer: ",res
  138. self.ssrcs = {}
  139. jingleSoup = BeautifulSoup(jingle)
  140. for cont in jingleSoup.iq.jingle.findAll('content'):
  141. if cont.description:
  142. self.ssrcs[cont['name']] = cont.description['ssrc']
  143. print "my ssrcs:",self.ssrcs
  144. gevent.joinall([
  145. gevent.spawn(self.advertiseSsrcs)
  146. ])
  147. def advertiseSsrcs(self):
  148. time.sleep(7)
  149. print "SSRC spammer started"
  150. while self.running:
  151. ssrcMsg = "<presence to='%(tojid)s' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%(nick)s</nick><stats xmlns='http://jitsi.org/jitmeet/stats'><stat name='bitrate_download' value='175'/><stat name='bitrate_upload' value='176'/><stat name='packetLoss_total' value='0'/><stat name='packetLoss_download' value='0'/><stat name='packetLoss_upload' value='0'/></stats><media xmlns='http://estos.de/ns/mjs'><source type='audio' ssrc='%(assrc)s' direction='sendre'/><source type='video' ssrc='%(vssrc)s' direction='sendre'/></media></presence>" % { 'tojid': "%s@%s/%s" % (ROOMNAME, ROOMDOMAIN, self.shortJid), 'nick': self.userId, 'assrc': self.ssrcs['audio'], 'vssrc': self.ssrcs['video'] }
  152. res = self.sendIq(ssrcMsg)
  153. print "reply from ssrc announce: ",res
  154. time.sleep(10)
  155. def xmppLoop(self):
  156. self.matrixCallId = time.time()
  157. res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' to='%s' xml:lang='en' wait='60' hold='1' content='text/xml; charset=utf-8' ver='1.6' xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), HOST))
  158. print res
  159. self.sid = res.body['sid']
  160. print "sid %s" % (self.sid)
  161. res = self.sendIq("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>")
  162. res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s' to='%s' xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), self.sid, HOST))
  163. res = self.sendIq("<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>")
  164. print res
  165. self.jid = res.body.iq.bind.jid.string
  166. print "jid: %s" % (self.jid)
  167. self.shortJid = self.jid.split('-')[0]
  168. res = self.sendIq("<iq type='set' id='_session_auth_2' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>")
  169. #randomthing = res.body.iq['to']
  170. #whatsitpart = randomthing.split('-')[0]
  171. #print "other random bind thing: %s" % (randomthing)
  172. # advertise preence to the jitsi room, with our nick
  173. res = self.sendIq("<iq type='get' to='%s' xmlns='jabber:client' id='1:sendIQ'><services xmlns='urn:xmpp:extdisco:1'><service host='%s'/></services></iq><presence to='%s@%s/d98f6c40' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%s</nick></presence>" % (HOST, TURNSERVER, ROOMNAME, ROOMDOMAIN, self.userId))
  174. self.muc = {'users': []}
  175. for p in res.body.findAll('presence'):
  176. u = {}
  177. u['shortJid'] = p['from'].split('/')[1]
  178. if p.c and p.c.nick:
  179. u['nick'] = p.c.nick.string
  180. self.muc['users'].append(u)
  181. print "muc: ",self.muc
  182. # wait for stuff
  183. while True:
  184. print "waiting..."
  185. res = self.sendIq("")
  186. print "got from stream: ",res
  187. if res.body.iq:
  188. jingles = res.body.iq.findAll('jingle')
  189. if len(jingles):
  190. self.callfrom = res.body.iq['from']
  191. self.handleInvite(jingles[0])
  192. elif 'type' in res.body and res.body['type'] == 'terminate':
  193. self.running = False
  194. del xmppClients[self.matrixRoom]
  195. return
  196. def handleInvite(self, jingle):
  197. self.initiator = jingle['initiator']
  198. self.callsid = jingle['sid']
  199. p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--jingle'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  200. print "raw jingle invite",str(jingle)
  201. sdp, out_err = p.communicate(str(jingle))
  202. print "transformed remote offer sdp",sdp
  203. inviteEvent = {
  204. 'offer': {
  205. 'type': 'offer',
  206. 'sdp': sdp
  207. },
  208. 'call_id': self.matrixCallId,
  209. 'version': 0,
  210. 'lifetime': 30000
  211. }
  212. matrixCli.sendEvent(self.matrixRoom, 'm.call.invite', inviteEvent)
  213. matrixCli = TrivialMatrixClient(ACCESS_TOKEN)
  214. gevent.joinall([
  215. gevent.spawn(matrixLoop)
  216. ])