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)
class Roster(FreshList): '''Manage the roster of the conference participants using Chronos sync. Additionally, it handles light-weight processing of the Peets Message. ''' __logger = Logger.get_logger('Roster') (Init, Joined, Stopped) = range(3) def __init__(self, chatroom_prefix, msg_callback, get_local_user, *args, **kwargs): ''' Args: chatroom_prefix (str): A broadcast prefix for the chatroom; this is used by Chronos for it's sync Interests. msg_callbck : The callback function when a Peets Message comes. get_local_user : A function that returns the local user information. ''' super(Roster, self).__init__(self.announce, self.reap_callback, *args, **kwargs) self.msg_callback = msg_callback self.get_local_user = get_local_user self.status = self.__class__.Init self.session = int(time()) self.peetsClosure = PeetsClosure(msg_callback = self.process_peets_msg) self.ccnx_sock = CcnxSocket() self.ccnx_sock.start() self.chronos_sock = SimpleChronosSocket(chatroom_prefix, self.fetch_peets_msg) # send join after 0.5 second self.schedule_next(0.5, self.announce) def fetch_peets_msg(self, name): '''A wrapper function for fetching peets msg. If the local user has stopped participanting, then don't fetch the peets msg. ''' if self.status == self.__class__.Stopped: return self.ccnx_sock.send_interest(name, self.peetsClosure) def process_peets_msg(self, interest, data): '''Process Peets Message. This is used as a callback for the PeetsClosure. Args: interest: The PyCCN.UpcallInfo.Interest. data: The PyCCN.UpcallInfo.ContentObject. Assume the interest for peets msg would have a name like this: /user-data-prefix/peets_msg/session/seq This is because in the current implementation of chronos, it is the naming convention to have both session and seq ''' # do not process remove msg when stopped if self.status == self.__class__.Stopped: return #name = data.name content = data.content #prefix = '/'.join(str(name).split('/')[:-2]) try: msg = PeetsMessage.from_string(content) uid = msg.user.uid if msg.msg_type == PeetsMessage.Join: #ru = RemoteUser(msg.user) #self[uid] = ru self.msg_callback(msg) elif msg.msg_type == PeetsMessage.Hello: try: self.announce_received(uid) except KeyError: self.__class__.__logger.info('Refresh announcement for unknown user %s, treating as Join', uid) #ru = RemoteUser(msg.user) #self[uid] = ru self.msg_callback(msg) elif msg.msg_type == PeetsMessage.Leave: #del self[uid] self.msg_callback(msg) elif msg.msg_type == PeetsMessage.Chat: self.msg_callback(msg) else: self.__class__.__logger.error("unknown PeetsMessage type") except KeyError as e: Roster.__logger.exception("PeetsMessage does not have type or from") # used by FreshList when zombie is reaped def reap_callback(self, remote_user): '''This is the reap callback for the FreshList. Do the clean up needed here when a remote user is considered left. Args: remote_user (RemoteUser): The remote user considered left by FreshList. ''' peets_msg = PeetsMessage(PeetsMessage.Leave, remote_user) self.msg_callback(peets_msg) print self.get_local_user().nick, 'reaping', remote_user def announce(self): '''This is a function to announce/refresh the prescence of the local user to others. ''' if self.status == self.__class__.Stopped: return user = self.get_local_user() msg_type = PeetsMessage.Hello if self.status == self.__class__.Joined else PeetsMessage.Join msg = PeetsMessage(msg_type, user) msg_str = str(msg) self.chronos_sock.publish_string(user.get_sync_prefix(), self.session, msg_str, StateObject.default_ttl) self.status = self.__class__.Joined def leave(self): '''Tell remote users that the local user is leaving. ''' user = self.get_local_user() msg_type = PeetsMessage.Leave msg = PeetsMessage(msg_type, user) msg_str = str(msg) self.chronos_sock.publish_string(user.get_sync_prefix(), self.session, msg_str, StateObject.default_ttl) self.status = self.__class__.Stopped # clean up our footprint in the chronos sync tree def clean_up(): self.chronos_sock.remove(user.get_sync_prefix()) print 'cleaning up' def clean_up(): self.chronos_sock.stop() self.shutdown() self.schedule_next(0.5, clean_up) # event loop thread should wait until we clean up self.schedule_next(0.5, clean_up)