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 __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 __init__(self, factory, pipe_size): ''' Args: factory (PeetsServerFactory) : the factory that stores necessory information about the local user pipe_size (int) : the pipeline size for fetching the remote media stream. Pipelining allows us to minimize impact of the interest-data roundtrip delay. ''' self.factory = factory self.pipe_size = pipe_size self.factory = factory self.factory.set_local_status_callback(self.toggle_scheduler) # here we use two sockets, because the pending interests sent by a socket can not be satisified # by the content published later by the same socket self.ccnx_int_socket = CcnxSocket() self.ccnx_int_socket.start() self.ccnx_con_socket = CcnxSocket() self.ccnx_con_socket.start() self.stream_closure = PeetsClosure(msg_callback = self.stream_callback, timeout_callback = self.stream_timeout_callback) self.probe_closure = PeetsClosure(msg_callback = self.probe_callback, timeout_callback = self.probe_timeout_callback) self.ctrl_probe_closure = PeetsClosure(msg_callback = self.ctrl_probe_callback, timeout_callback = self.ctrl_probe_timeout_callback) self.scheduler = None self.peets_status = None
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 PeetsMediaTranslator(DatagramProtocol): ''' A translator protocol to relay local udp traffic to NDN and remote NDN traffic to local udp. This class also implements the strategy for fetching remote data. If the remote seq is unknown, use a short prefix without seq to probe; otherwise use a naive leaking-bucket like method to fetch the remote data We seperate the fetching of the media stream and the fetching of the control stream (RTCP, STUN, etc). ''' __logger = Logger.get_logger('PeetsMediaTranslator') def __init__(self, factory, pipe_size): ''' Args: factory (PeetsServerFactory) : the factory that stores necessory information about the local user pipe_size (int) : the pipeline size for fetching the remote media stream. Pipelining allows us to minimize impact of the interest-data roundtrip delay. ''' self.factory = factory self.pipe_size = pipe_size self.factory = factory self.factory.set_local_status_callback(self.toggle_scheduler) # here we use two sockets, because the pending interests sent by a socket can not be satisified # by the content published later by the same socket self.ccnx_int_socket = CcnxSocket() self.ccnx_int_socket.start() self.ccnx_con_socket = CcnxSocket() self.ccnx_con_socket.start() self.stream_closure = PeetsClosure(msg_callback = self.stream_callback, timeout_callback = self.stream_timeout_callback) self.probe_closure = PeetsClosure(msg_callback = self.probe_callback, timeout_callback = self.probe_timeout_callback) self.ctrl_probe_closure = PeetsClosure(msg_callback = self.ctrl_probe_callback, timeout_callback = self.ctrl_probe_timeout_callback) self.scheduler = None self.peets_status = None def toggle_scheduler(self, status): '''Start or stop the scheduler for periodic jobs. Args: status (str): either 'Running' or 'Stopped' ''' if status == 'Running': self.peets_status = 'Running' self.scheduler = Scheduler() self.scheduler.start() self.scheduler.add_interval_job(self.fetch_media, seconds = 0.01, max_instances = 2) elif status == 'Stopped': self.peets_status = 'Stopped' for job in self.scheduler.get_jobs(): self.scheduler.unschedule_job(job) self.scheduler.shutdown(wait = True) self.scheduler = None def datagramReceived(self, data, (host, port)): '''Intercept the webrtc traffice from the local front end and relay it to the NDN Args: data (bytes) : the UDP data host (str) : the IP of the source port (int) : the port of the source 1. Differentiate RTP vs RTCP RTCP: packet type (PT) = 200 - 208 SR (sender report) 200 RR (receiver report) 201 SDES (source description) 202 BYE (goodbye) 203 App (application-defined) 204 other types go until 208 RFC 5761 (implemented by WebRTC) makes sure that RTP's PT field plus M field (which is equal to the PT field in RTCP) would not conflict 2. Differentiate STUN vs RTP & RTCP STUN: the most significant 2 bits of every STUN msg MUST be zeros (RFC 5389) RTP & RTCP: version bits (2 bits) value equals 2 Note: Tried to fake a Stun request and response so that we don't have to relay stun msgs to NDN, but failed. It worked for a time, although will significantly high rate of the STUN message exchanges We need to use the username exchanged in the sdps for stun it worked for a while but magically stopped working, so now we still send it over NDN Note 2: We only publish one medai stream from the local user (with the default offer SDP). We publish RTCP and STUN for each PeerConnections though. ''' # mask to test most significant 2 bits msg = bytearray(data) c = self.factory.client if msg[0] & 0xC0 == 0 or msg[1] > 199 and msg[1] < 209: try: ctrl_seq = c.ctrl_seqs[port] cid = c.remote_cids[port] # RTCP and STUN is for each peerconnection. the cid of remote user is used to identify the peer connection so that remote user knows which one to fetch name = c.local_user.get_ctrl_prefix() + '/' + cid + '/' + str(ctrl_seq) c.ctrl_seqs[port] = ctrl_seq + 1 self.ccnx_con_socket.publish_content(name, data) except KeyError: pass elif c.media_source_port == port: # only publish one media stream name = c.local_user.get_media_prefix() + '/' + str(c.local_seq) c.local_seq += 1 self.ccnx_con_socket.publish_content(name, data)
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)