Hier soir, je n'arrivais pas à dormir... J'ai donc décidé de regarder la Freebox sur mon portable, en Wifi depuis mon lit...

Le problème en Wifi, c'est qu'il y a beaucoup trop de paquets perdus... et comme le flux est en UDP, ils ne sont pas retransmis... Donc, ça coupe tout le temps et on a toujours plein de gros carrés.

Je me suis donc mis en tête d'écrire un petit proxy RTSP vers HTTP. Mon postulat de départ est simple. En HTTP, grâce a une liaison TCP, la qualité devrait être meilleur, vu que les paquets perdus sont retransmis (en contrepartie, il faut un buffer pour pouvoir attendre que tout les paquet soient arrivé et dans l'ordre avant de les jouer, mais ça c'est le player qui s'en occupe...). D'autre part, le fait de disposer d'un flux http devrais permettre de lire le flux avec des players qui ne gèrent pas le RTSP (comme QuikTime Player ou des périphériques dédié). Enfin, je me suis dit que l'écriture d'un truc comme ça devait être simple est marrante...

J'ai donc commencé à essayer de le faire en C... Puis je me suis rappelé le nombre de lignes de code qu'il faut aligner avant de pouvoir mettre en place un socket, et aussi que la gestion des string était inexistante... J'ai donc décidé d'essayer d'écrire ça en python... Comme je viens de me mettre à ce langage, je me suis dit que ça pouvait être un bon exercice...

L'algorithme est plutôt simple :
  • On met en place un socket serveur pour attendre une connexion HTTP de la part du client
  • On récupère l'url demandé par le client, et on lui renvois le header de réponse
  • On se connecte à la Freebox, et on demande a jouer le flux demandé (via l'url)
  • On met en place un socket UDP prêt à recevoir le flux de la Freebox
  • Et enfin, on répète toutes les données envoyées sur le port UDP vers le client
Voilà le code que ça donne (Télécharger) :
import socket
import sys

# une petire class de serveur qui me simplifie un petit peu le travail
class server_socket(socket.socket):
	def __init__(self, arg):
		if not type(arg)==tuple:
			self.socket=arg
		else:
			self.socket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
			self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
			self.socket.bind(arg)
			self.socket.listen(1)
	
	def accept(self):
		s, addr=self.socket.accept()
		return server_socket(s), addr
	
	def readline(self):
		"""receive and return a complete line"""
		line=""
		while 1:
			char = self.recv(1)
			if not char:
				if line=="":
					return False
				return line
			if char=="\n":
				return line
			if char!="\r":
				line += char
	
	def __getattr__(self, name):
		return getattr(self.socket, name)

# une petite class de client.. la même que pour un serveur, sauf l'initialisation
class client_socket(server_socket):
	def __init__(self, ip, port):
		self.socket=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.socket.connect((ip,port))

# une class qui essaye de comprendre les réponses au requettes RTSP
class basic_rtsp_socket(client_socket):
	def __init__(self, ip, port):
		client_socket.__init__(ip, port)
		self.cseq=1
		
	def request(self, request):
		print "\n".join([">>> %s" % x for x in request.split("\n")])
		self.send(request % self.cseq)
		self.cseq+=1
		l=self.readline()
		print "<<< %s" % l
		(proto, code)=l.split(" ")[0:2]
		if proto[0:4]!="RTSP": raise "Error: not a rtsp response", l
		if code!="200": raise "Error: rtsp error", l
		
		l=self.readline()
		print "<<< %s" % l
		header={}
		while l!="" and l!=None:
			l2=l.split(": ")
			if len(l2)!=2: raise "Incorrect response", l
			header[l2[0]]=l2[1]
			l=self.readline()
			print "<<< %s" % l
		if l==None: raise "rtsp connection closed by foreign host", None
		data=None
		if "Content-length" in header.keys():
			data=read(int(header["Content-length"]))
		return (header, data)


# Début du code

# on met en place un serveur http
http_listen=server_socket(("", 8082))
http_conn, addr = http_listen.accept()
print 'Connected by', addr

data = http_conn.readline()
if not data: raise "no data on http line", None
# on prend la première ligne
(request, url, proto)=data.split(" ")[0:3]
if request!="GET":
	raise "Error : not a get request", data
if proto[0:4]!="HTTP":
	raise "Error : not a http request", data
# on attend la fin de la requette
# soit une ligne vide
# un peu barbare... mais je suis qur qu'il ren a dire d'interressant
l=http_conn.readline()
print "<<< %s" %l
while l!="" and l!=None:
	l=http_conn.readline()
	print "<<< %s" % l
	# parle a ma main !!!
if l==None: raise "http connection closed", None

# on repond le header
http_conn.send("""HTTP/1.0 200 OK
Content-type: application/octet-stream
Cache-Control: no-cache

""")

# on établie la connexion rtsp
rtsp_session=client_socket("mafreebox.freebox.fr", 554)
print "connected to the rtsp session"
# dump du dialogue RTSP entre VLC et la freebox...
(header, data)=rtsp_session.request("""OPTIONS rtsp://mafreebox.freebox.fr/freeboxtv%s RTSP/1.0
CSeq: %s
User-Agent: rtsp2http v0.0.1

""" % (url, "%d"))
(header, data)=rtsp_session.request("""SETUP rtsp://212.27.38.253/freeboxtv%s RTSP/1.0
CSeq: %s
Transport: RTP/AVP;unicast;client_port=1662-1663
User-Agent: rtsp2http v0.0.1

""" % (url, "%d"))
session=header["Session"]
# on se prépare a recevoir les connexion UDP
rtsp_data = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
rtsp_data.bind(("", 1662))
# Go, go, go
(header, data)=rtsp_session.request("""PLAY rtsp://mafreebox.freebox.fr/freeboxtv%s RTSP/1.0
CSeq: %s
Session: %s
Range: npt=0.000-
User-Agent: rtsp2http v0.0.1

""" % (url, "%d", session))

# on repete tout au client http
ok=True
while ok:
	data, addr=rtsp_data.recvfrom(1024)
	if addr[0]==socket.gethostbyname("mafreebox.freebox.fr"):
		try:
			http_conn.sendall(data)
			# il y a peut etre besoin d'une conversion...
			# mais j'espere pas...
			# a creuser
		except:
			ok=False # une execption... on arrete tout !


# une des connexion a été coupée, on ferme toutes les connexions...
rtsp_session.request("""TEARDOWN rtsp://mafreebox.freebox.fr/freeboxtv%s RTSP/1.0
CSeq: %s
Session: %s
User-Agent: rtsp2http v0.0.1

""" % (url, "%d", session))
rtsp_session.close()
rtsp_data.close()
http_listen.close()
http_conn.close()

Pour l'instant, VLC arrive à se connecter, le proxy se connecte à la Freebox et commence à répéter le flux... mais rien a ne s'affiche à l'écran...

Bon il était déjà 2h30 et maintenant, j’avais vraiment envie de dormir… Mission réussie… Je suis aller me coucher ;-)

J'ai deux piste à creuser pour essayer de faire marcher ça : Vérifier que toutes les données sont bien retransmises (problème de taille de buffer, ou de vitesse (paquets dropé) par exemple), ou vérifier si je peux bien renvoyer le flux brute de cette façon, et s'il ne faut pas le convertir et el remettre en forme...

Affaire à suivre ;-)

Update 12/02/2006 : J'ai débugé le script.
Update 21/02/2006 : Quelques modifications.