Exemplo n.º 1
0
class PeetsServerFactory(WebSocketServerFactory):
  '''A factory class that does housing keeping job. This is needed when we use the proxy for multiple local users (each of the user would prompts the creation of a PeetsServerProtocol instance).
  Although the current usage does not support multiple local user, it's here due to historical reason and potential future needs.
  '''

  __logger = Logger.get_logger('PeetsServerFactory')
  
  def __init__(self, udp_port, nick, prefix, chatroom, url = None, protocols = [], debug = False, debugCodePaths = False):
    '''
    Args:
      udp_port (int) : The udp port to listen for WebRTC traffice.
      nick (str) : The nickname for the local user.
      prefix (str) : The prefix for the local user.
      chatroom (str) : The chatroom to join.

    Kwargs:
      url : The url for websocket server
      And other default kwargs of WebSocketServerFactory.

    A roster for the chatroom is maintained here.
    CcnxSocket created for PeetsMessages propagation if NDN.
    '''
    # super can only work with new style classes which inherits from object
    # apparently WebSocketServerFactory is old style class
    WebSocketServerFactory.__init__(self, url = url, protocols = protocols, debug = debug, debugCodePaths = debugCodePaths)
    self.handlers = {'join_room' : self.handle_join, 'send_ice_candidate' : self.handle_ice_candidate, 'send_offer' : self.handle_offer, 'media_ready' : self.handle_media_ready, 'chat_msg': self.handle_chat}
    # keep the list of local clients, first, we deal with the case where there is only one local client
    self.client = None
    # keep the list of remote users
    self.roster = None
    self.listen_port = udp_port
    self.__class__.__logger.debug('UDP-PORT=%s', str(udp_port))
    self.ccnx_socket = CcnxSocket()
    self.ccnx_socket.start()
    self.local_status_callback = lambda status: 0
    self.nick = nick
    self.prefix = prefix
    self.chatroom = chatroom

  def set_local_status_callback(self, callback):
    self.local_status_callback = callback

  def sdp_callback(self, interest, data):
    '''A callback function for incoming sdp description from remote users.

    Args:
      interest : PyCCN.UpcallInfo.Interest
      data : PyCCN.UpcallInfo.ContentObject

    Send the sdp to the frontend. If we already received the ICE candidate for the same remote user, then we also send out this ICE candidate.
    '''
    content = data.content
    offer_msg = RTCMessage.from_string(content)
    d = RTCData(socketId = self.client.id, sdp = offer_msg.data.sdp)
    # this is the answer to the local user
    answer_msg = RTCMessage('receive_answer', offer_msg.data)
    self.client.sendMessage(str(answer_msg))
    remote_user = self.roster[offer_msg.data.socketId]
    remote_user.set_sdp_sent()
    # we received ice candidate before sending answer
    if remote_user.ice_candidate_msg is not None:
      self.client.sendMessage(str(remote_user.ice_candidate_msg))

  def peets_msg_callback(self, peets_msg):
    '''A callback function to process the peets message (to be used by Roster).

    Args:
      peets_msg (PeetsMessage) : The received PeetsMessage.

    Basically, it needs to inform the status change or text chat to the front end and als fetch sdp for the new remote user if the PeetsMessage is a Join.
    '''
    remote_user = RemoteUser(peets_msg.user)
    if peets_msg.msg_type == PeetsMessage.Join or peets_msg.msg_type == PeetsMessage.Hello:
      if self.roster.has_key(remote_user.uid):
        self.__class__.__logger.debug("Redundant join message from %s", remote_user.get_sync_prefix())
        exit(0)
        return
      
      self.roster[remote_user.uid] = remote_user
      self.__class__.__logger.debug("Peets join message from remote user: %s", remote_user.get_sync_prefix())
      data = RTCData(socketId = remote_user.uid, username= remote_user.nick)
      msg = RTCMessage('new_peer_connected', data)
      self.client.sendMessage(str(msg))
      name = remote_user.get_sdp_prefix()
      
      # ask for sdp message for the new remote user
      self.ccnx_socket.send_interest(name, PeetsClosure(msg_callback = self.sdp_callback))
      

    elif peets_msg.msg_type == PeetsMessage.Leave:
      del self.roster[remote_user.uid]
      self.__class__.__logger.debug("Peets leave message from remote user: %s", remote_user.get_sync_prefix())
      data = RTCData(socketId = remote_user.uid)
      msg = RTCMessage('remove_peer_connected', data)
      self.client.sendMessage(str(msg))

    elif peets_msg.msg_type == PeetsMessage.Chat:
      data = RTCData(socketId = remote_user.uid, messages = peets_msg.extra, username = remote_user.nick)
      msg = RTCMessage('receive_chat_msg', data)
      self.client.sendMessage(str(msg))

  def unregister(self, client):
    '''Clean up when the local user quits.

    Args:
      client (PeetsServerProtocol) : the local user.
    '''

    if self.client is not None and client.id == self.client.id:
      self.local_status_callback('Stopped')
      self.handle_leave(client)
      PeetsServerFactory.__logger.debug("unregister client %s", client.id)
      self.client = None
      self.roster = None

  def process(self, client, msg):
    '''Process the message from the local user's front end.

    Args:
      client (PeetsServerProtocol) : local user.
      msg (RTCMessage) : the message from frontend.
    '''
    rtcMsg = RTCMessage.from_string(msg)
    handler = self.handlers.get(rtcMsg.eventName)
    if handler is not None:
      handler(client, rtcMsg.data)
    else:
      PeetsServerFactory.__logger.error("Unknown event name: " + rtcMsg.eventName)

  def handle_join(self, client, data):
    '''Handle join message from the frontend

    Args:
      client : local user
      data : join message from local user
    '''
    PeetsServerFactory.__logger.debug('join from client %s', client.id)

    d = RTCData(connections = [])
    msg = RTCMessage('get_peers', d)
    client.sendMessage(str(msg))
    client.local_user.nick = self.nick
    client.local_user.prefix = self.prefix

  def handle_media_ready(self, client, data):
    '''Announce self to the NDN network when the local user has media ready (i.e. get permission fo use microphone and/or camera)

    Args:
      client : local user
      data : media_ready message from local user
    '''
    if self.client is None:
      PeetsServerFactory.__logger.debug('register client %s', client.id)
      self.client = client
      # announce self in NDN
      self.roster = Roster('/ndn/broadcast/' + self.chatroom, self.peets_msg_callback, lambda: self.client.local_user)
      self.local_status_callback('Running')
    else:
      PeetsServerFactory.__logger.debug("Join message from: %s, but we already have a client: %s", client.id, self.client.id)

  def handle_leave(self, client):
    '''Clean up when local user leaves.
    
    Args:
      client : local user.
    '''
    self.roster.leave()
    sleep(1.1)
    

  # this method is local, i.e. no leak to NDN
  def handle_ice_candidate(self, client, data):
    '''Handle ice candidate from local user.

    Args:
      client : local user
      data : ice candidate message from local user.

    Whenever the local frontend ends its ice candidate, it's expecting the ice candiate from the remote end.
    We will use a fake ice candidate for the remote end to trick the frontend to use an ice candidate that points to a port that our proxy is listening to intercept webrtc traffice.
    If the SDP for remote end has not been received yet, we postpone the reply of ice candidate because WebRTC requires SDP to be set up before receiving ice candidate message.
    '''
    candidate = Candidate.from_string(data.candidate)
    if client.media_sink_ports.get(data.socketId) is None:
      port = int(candidate.port)
      client.media_sink_ports[data.socketId] = port
      client.ctrl_seqs[port] = 0
      client.remote_cids[port] = data.socketId
      if client.media_source_port is None:
        client.media_source_port = int(candidate.port)
      if client.ip is None:
        client.ip = candidate.ip
      
      candidate = Candidate(('127.0.0.1', str(self.listen_port)))
      d = RTCData(candidate = str(candidate), socketId = data.socketId)
      msg = RTCMessage('receive_ice_candidate', d)
      remote_user = self.roster[data.socketId]
      remote_user.set_ice_candidate_msg(msg)
      # sdp answer has already been sent
      if remote_user.sdp_sent:
        self.client.sendMessage(str(msg))

      
  def handle_offer(self, client, data):
    '''Handle the offer (sdp) from the front end

    Args:
      client : local user
      data : offer sdp message

    We store the offer for later use (we will use the same offer for all the PeerConnection this local user is going to establish) and also publish it to NDN.
    '''
    if client.media_source_sdp is None:
      client.media_source_sdp = data.sdp

    d = RTCData(sdp = client.media_source_sdp, socketId = client.id)
    msg = RTCMessage('receive_offer', d)

    name = client.local_user.get_sdp_prefix() 
    # publish sdp msg
    self.ccnx_socket.publish_content(name, str(msg))


    def publish(interest):
      self.ccnx_socket.publish_content(name, str(msg))

    self.ccnx_socket.register_prefix(name, PeetsClosure(incoming_interest_callback = publish))


  def has_local_client(self):
    return self.client is not None and self.roster is not None

  def handle_chat(self, client, data):
    '''Handle chat message from local user.

    Args:
      client : local user
      data : chat message from local user

    Publish the text chat message to NDN.
    '''
    msg = PeetsMessage(PeetsMessage.Chat, self.client.local_user, extra = data.messages)
    self.roster.chronos_sock.publish_string(self.client.local_user.get_sync_prefix(), self.roster.session, str(msg), StateObject.default_ttl)