def _create_conf(self): fd, conf_path = tempfile.mkstemp(suffix='.conf') conf = "start {}\nend {}\ninterface {}\nnotify_file dumpleases\noption subnet {}\noption lease {}\n".format( Settings().peeraddress, Settings().peeraddress, self.interface, Settings().netmask, Settings().timeout) with open(conf_path, 'w') as c: c.write(conf) return conf_path
def __init__(self, player, logger='picast'): super(RtspSink, self).__init__(name='rtsp-server-0', daemon=True) self.config = Settings() self.logger = getLogger(logger) self.player = player self.watchdog = 0 self.csnum = 0 self.daemon = True self.video = RasberryPiVideo() self.wfd_parameters = self.config.get_wfd_parameters() self.wfd_video_formats = self.video.get_wfd_video_formats()
def __init__(self, interface: str, logger='picast'): """Constructor accept an interface to listen.""" self.config = Settings() self.dhcpd = None self.interface = interface self.logger = getLogger(logger) self.conf_path = self._create_conf()
def main(arg: Optional[Any] = None): parser = argparse.ArgumentParser( prog='picast', description='picast', formatter_class=argparse.RawTextHelpFormatter, add_help=True) parser.add_argument("--debug", action="store_true", help="Verbose debug output") parser.add_argument("-c", "--config", help="Specify configuration file.") args = parser.parse_args(arg) if args.config: config_file = args.config if os.path.exists(config_file) and os.path.isfile(config_file): pass else: config_file = None else: config_file = None # ------------------- start of configurations # it should call first: load configurations from ini files config = Settings(config=config_file) package_dir = os.path.join(os.path.dirname(__file__)) if config.logging_config: logging_ini = config.logging_config if not os.path.exists(logging_ini): if os.path.exists(os.path.join(package_dir, logging_ini)): logging_ini = os.path.join(package_dir, logging_ini) else: logging_ini = os.path.join(package_dir, "logging.ini") LoggingConfig.fileConfig(logging_ini) logger = getLogger(config.logger) if args.debug: logger.setLevel("DEBUG") if config.player == "gst": player = GstPlayer() # type: Optional[Union[GstPlayer, VlcPlayer]] elif config.player == "vlc": player = VlcPlayer() else: player = None logger.fatal("FATAL: Unknown player name option!: {}".format( config.player)) exit(1) # ------------------- end of configurations wifip2p = WifiP2PServer() rtspsink = RtspSink(player) wifip2p.start() rtspsink.start() rtspsink.join() wifip2p.join()
def __init__(self): self.config = Settings() self.native = 0x06 self.preferred = 0 self.profile = 0x01 self.level = 0x10 with open(os.path.join(os.path.dirname(__file__), 'resolutions.json'), 'r') as j: self.resolutions = json.load(j)[0] self._get_display_resolutions()
def test_config_override(): fd, temp_path = tempfile.mkstemp() with open(temp_path, "w") as f: f.write("[player]\n") f.write("name=nop\n") Settings()._config = None # Clean singletone state assert Settings().device_name == 'picast' assert Settings().player == 'vlc' Settings()._config = None # Clena singletone state assert Settings(config=temp_path).device_name == 'picast' assert Settings(config=temp_path).player == 'nop' Settings()._config = None # Clena singletone state os.unlink(temp_path)
def test_config_device_name(): assert Settings().device_name == 'picast'
def test_config_rtp_port(): assert Settings().rtp_port == 1028
def test_config_rtsp_port(): assert Settings().rtsp_port == 7236
class RtspSink(threading.Thread): def __init__(self, player, logger='picast'): super(RtspSink, self).__init__(name='rtsp-server-0', daemon=True) self.config = Settings() self.logger = getLogger(logger) self.player = player self.watchdog = 0 self.csnum = 0 self.daemon = True self.video = RasberryPiVideo() self.wfd_parameters = self.config.get_wfd_parameters() self.wfd_video_formats = self.video.get_wfd_video_formats() def get_rtsp_headers(self) -> Dict[str, Optional[str]]: headers = self.read_headers() results = {} firstline = headers[0] regex = re.compile(r"RTSP/1.0 ([0-9]+) (\w+([ ]\w)*)") if firstline.startswith('RTSP/1.0'): m = regex.match(firstline) if m: status, reason = m.group(1, 2) cmd = None url = None resp = "{} {}".format(status, reason) # type: Optional[str] else: raise ValueError else: cmd, url, version = firstline.split(' ') if version != 'RTSP/1.0': raise ValueError resp = None results['cmd'] = cmd results['url'] = url results['resp'] = resp for h in headers[1:]: pos = h.find(':') if not pos: break key = h[:pos] val = h[pos + 2:] results[key] = val return results def read_headers(self) -> List[str]: headers = [] line = self.sock.readline() while not line: line = self.sock.readline() while line: headers.append(line.decode('UTF-8')) line = self.sock.readline() self.logger.debug("<< {}".format(headers)) return headers def read_body(self, headers) -> bytes: length = headers.get('Content-Length', None) if length is None: return b'' length = int(length) return self.sock.read(length) @staticmethod def _rtsp_response_header(cmd: Optional[str] = None, url: Optional[str] = None, res: Optional[str] = None, seq: Optional[str] = None, others: Optional[List[Tuple[str, str]]] = None) -> str: if cmd is not None: msg = "{0:s} {1:s} RTSP/1.0".format(cmd, url) else: msg = "RTSP/1.0" if res is not None: msg += ' {0}\r\nCSeq: {1}\r\n'.format(res, seq) else: msg += '\r\nCSeq: {}\r\n'.format(seq) if others is not None: for k, v in others: msg += '{}: {}\r\n'.format(k, v) msg += '\r\n' return msg @staticmethod def _parse_transport_header(data: str) -> Tuple[bool, str, str]: """ Parse Transport header value such as "Transport: RTP/AVP/UDP;unicast;client_port=1028;server_port=5000" """ udp = True client_port = '0' server_port = '0' paramlist = data.split(';') for p in paramlist: if p.startswith('RTP'): rtp, avp, prot = p.split('/') if prot == 'UDP': udp = True elif prot == 'TCP': udp = False else: raise ValueError elif p.startswith('unicast'): pass elif p.startswith('client_port'): _, client_port = p.split('=') elif p.startswith('server_port'): _, server_port = p.split('=') else: continue return udp, client_port, server_port def rtsp_m1(self) -> bool: headers = self.get_rtsp_headers() if headers['cmd'] != 'OPTIONS': return False s_data = self._rtsp_response_header(seq=headers['CSeq'], res="200 OK", others=[("Public", "org.wfa.wfd1.0, SET_PARAMETER, GET_PARAMETER")]) self.logger.debug("<-{}".format(s_data)) self.sock.write(s_data.encode('ASCII')) return True def rtsp_m2(self) -> bool: self.csnum = 100 s_data = self._rtsp_response_header(seq=str(self.csnum), cmd="OPTIONS", url="*", others=[('Require', 'org.wfa.wfd1.0')]) self.logger.debug("<-{}".format(s_data)) self.sock.write(s_data.encode('ASCII')) headers = self.get_rtsp_headers() if headers['CSeq'] != '100' or headers['resp'] != "200 OK": return False return True def rtsp_m3(self) -> bool: headers = self.get_rtsp_headers() if headers['cmd'] != 'GET_PARAMETER' or headers['url'] != 'rtsp://localhost/wfd1.0': return False body = self.read_body(headers) msg = '' for req in body.decode('UTF-8').split('\r\n'): if req == '': continue elif req == 'wfd_client_rtp_ports': msg += "wfd_client_rtp_ports: RTP/AVP/UDP;unicast {} 0 mode=play\r\n".format(self.config.rtp_port) elif req == 'wfd_video_formats': msg += 'wfd_video_formats: {}\r\n'.format(self.wfd_video_formats) elif req in self.wfd_parameters: msg += '{}: {}\r\n'.format(req, self.wfd_parameters[req]) else: msg += '{}: none\r\n'.format(req) m3resp = self._rtsp_response_header(seq=headers['CSeq'], res="200 OK", others=[('Content-Type', 'text/parameters'), ('Content-Length', str(len(msg))) ]) m3resp += msg self.logger.debug("<-{}".format(m3resp)) self.sock.write(m3resp.encode('ASCII')) return True def rtsp_m4(self) -> bool: headers = self.get_rtsp_headers() if headers['cmd'] != "SET_PARAMETER" or headers['url'] != "rtsp://localhost/wfd1.0": return False self.read_body(headers) # FIXME: parse body here to retrieve video mode and set actual mode. s_data = self._rtsp_response_header(res="200 OK", seq=headers['CSeq']) self.logger.debug("<-{}".format(s_data)) self.sock.write(s_data.encode('ASCII')) return True def rtsp_m5(self) -> bool: headers = self.get_rtsp_headers() self.read_body(headers) if headers['cmd'] != 'SET_PARAMETER': self.logger.debug("M5: got other than SET_PARAMETER request.") s_data = self._rtsp_response_header(res="400 Bad Requests", seq=headers['CSeq']) self.logger.debug("<-{}".format(s_data)) self.sock.write(s_data.encode('ASCII')) return False # FIXME: analyze body to have 'wfd_trigger_method: SETUP' s_data = self._rtsp_response_header(res="200 OK", seq=headers['CSeq']) self.logger.debug("<-{}".format(s_data)) self.sock.write(s_data.encode('ASCII')) return True def rtsp_m6(self) -> Tuple[Optional[str], Optional[str]]: self.csnum += 1 sessionid = None server_port = None m6req = self._rtsp_response_header(cmd="SETUP", url="rtsp://{0:s}/wfd1.0/streamid=0".format(self.config.peeraddress), seq=str(self.csnum), others=[ ('Transport', 'RTP/AVP/UDP;unicast;client_port={0:d}'.format(self.config.rtp_port)) ]) self.logger.debug("<-{}".format(m6req)) self.sock.write(m6req.encode('ASCII')) headers = self.get_rtsp_headers() if headers['CSeq'] is not None and headers['CSeq'] != str(self.csnum): raise ValueError('Unmatch sequence number: {}'.format(headers['CSeq'])) if 'Transport' in headers: assert headers['Transport'] is not None udp, client_port, server_port = self._parse_transport_header(headers['Transport']) self.logger.debug("server port {}".format(server_port)) if 'Session' in headers: assert headers['Session'] is not None sessionid = headers['Session'].split(';')[0] return sessionid, server_port def rtsp_m7(self, sessionid: str) -> bool: self.csnum += 1 m7req = self._rtsp_response_header(cmd='PLAY', url='rtsp://{0:s}/wfd1.0/streamid=0'.format(self.config.peeraddress), seq=str(self.csnum), others=[('Session', sessionid)]) self.logger.debug("<-{}".format(m7req)) self.sock.write(m7req.encode('ASCII')) headers = self.get_rtsp_headers() if headers['resp'] != "200 OK" or headers['CSeq'] != str(self.csnum): return False return True def negotiate(self) -> bool: self.logger.debug("---- Start negotiation ----") while True: if not self.rtsp_m1(): break if not self.rtsp_m2(): break if not self.rtsp_m3(): break if not self.rtsp_m4(): break if not self.rtsp_m5(): break sessionid, server_port = self.rtsp_m6() if sessionid is None: break if not self.rtsp_m7(sessionid): break self.logger.info("---- Negotiation successful ----") return True self.logger.info("---- Negotiation failed ----") return False def is_keep_alive(self, headers): return headers['cmd'] == "GET_PARAMETER" and headers['url'] == "rtsp://localhost/wfd1.0" def is_parameter_change(self, headers): return headers['cmd'] == "SET_PARAMETER" def keep_alive(self, headers): self.read_body(headers) resp_msg = self._rtsp_response_header(seq=headers['CSeq'], res="200 OK") self.sock.write(resp_msg.encode('ASCII')) return def parameter_change(self, headers): body = self.read_body(headers) lines = body.decode('UTF-8').splitlines() if 'wfd_trigger_method: TEARDOWN' in lines: self.logger.debug("Got TEARDOWN request.") self.response_teardown(headers) self.teardown = True return def response_teardown(self, headers): resp_msg = self._rtsp_response_header(seq=headers['CSeq'], res="200 OK") self.sock.write(resp_msg.encode('ASCII')) m5_msg = self._rtsp_response_header(seq=str(self.csnum), cmd="TEARDOWN", url="rtsp://localhost/wfd1.0") self.sock.write(m5_msg.encode('ASCII')) def is_response(self, headers): return headers['resp'] == "200 OK" def play(self) -> None: self.player.start() self.teardown = False self.watchdog = 0 self.sock.settimeout(10) while True: try: headers = self.get_rtsp_headers() except socket.error as e: err = e.args[0] if err == errno.EAGAIN or err == errno.EWOULDBLOCK or err == errno.EALREADY or err == errno.EINPROGRESS: sleep(0.1) continue elif err == errno.ETIMEDOUT: self.watchdog += 1 if self.watchdog > self.config.max_timeout: break else: self.logger.debug("Got unexpected socket error {}".format(e.args[0])) break else: self.watchdog = 0 if self.is_keep_alive(headers): self.keep_alive(headers) elif self.is_parameter_change(headers): self.parameter_change(headers) if self.teardown: self.player.stop() elif self.is_response(headers): if self.teardown: break else: # ignore all other commands now... continue def run(self): sd = ServiceDiscovery() sd.register() while True: self.sock = RTSPTransport(self.config.peeraddress, self.config.rtsp_port) if self.negotiate(): self.play() self.sock.close() sleep(1)
def test_config_group_name(): assert Settings().group_name == 'persistent'
def test_config_player_log_file(): assert Settings().player_log_file == '/var/tmp/player.log'
def __init__(self, logger='picast'): self.config = Settings() self.logger = getLogger(logger) Gst.init(None)
def test_config_pin(): assert Settings().pin == '12345678'
def test_config_device_type(): assert Settings().device_type == '7-0050F204-4'
def test_config_gst_decoder(): assert Settings().gst_decoder == 'omxh264dec'
def test_config_max_timeout(): assert Settings().max_timeout == '10'
def test_config_wps_mode(): assert Settings().wps_mode == 'pin'
def test_config_recreate_group(): assert Settings().recreate_group == False
def test_config_logger(): assert Settings().logger == 'picast'
def test_config_player(): assert Settings().player == 'vlc'
def test_config_logging_config(): assert Settings().logging_config == 'logging.ini'
def test_config_player_custom_args(): assert Settings().player_custom_args == []
def test_config_myaddress(): assert Settings().myaddress == '192.168.173.1'
def test_config_peeraddress(): assert Settings().peeraddress == '192.168.173.80'
def test_config_netmask(): assert Settings().netmask == '255.255.255.0'
def __init__(self): self.config = Settings() self.logger = getLogger(self.config.logger) self.zc = zeroconf.Zeroconf()
def __init__(self, R2=False, logger='picast'): super(WifiP2PServer, self).__init__(name='wifi-p2p-0', daemon=False) self.config = Settings() self.logger = getLogger(logger) self.set_p2p_interface(R2)