Ayant une FreeBox depuis pas mal d'années mais pas de poste de télévision, je regarde parfois des émissions par le flux IP de la FreeBox sous VLC. J'ai développé un PVR (personnal video recorder) minimaliste, sous Linux, qui tient en quelques scripts Python et PHP.
Abonné à internet chez Free depuis la FreeBox 2, et n'ayant pas de poste de télévision, je regarde parfois des émissions grâce au flux IP TV sur le réseau (n'ayant pas de prise antenne dans mon logement, les chaînes du groupe TF1 et M6 ne sont pas utilisables; pas grave).
Par différentes étapes, j'ai développé un PVR minimaliste ne nécessitant pas d'installer un système complexe. Il tourne sous GNU-Debian-Linux avec une interface web très simple, ne nécessitant qu'un navigateur web et VLC coté poste client.
Flux TV
Accès en HTTP multiposte
De manière simple, VLC suffit pour voir les chaînes de la FreeBox. Il faut ouvrir un flux réseau avec l'adresse RTSP que l'on retrouve dans la playlist de la FreeBox TV; exemple pour France 5 en HD : rtsp://mafreebox.freebox.fr/fbxtv_pub/stream?namespace=1&service=203&flavour=hd.
Il existe même des plugins VLC pour simplifier l'accès à cette liste.
Pour commencer, j'ai adapté le script Pyhton d'AlexSoloex Puyb qui fait office de proxy RTSP/RTP/RTCP vers HTTP et permet donc d'avoir un multiposte accessible depuis tout lecteur vidéo supportant le streaming à travers HTTP comme, par exemple, VLC sous Linux, Windows et Android.
Ce script implémente un serveur HTTP qui doit recevoir l'URL de la FreeBox et lance la requête RTSP puis encapsule les trames RTP dans la réponse HTTP. Ce serveur gère le protocol RTCP pour maintenir la diffusion du flux TV. Il peut gérer 20 diffusions en parallèle (simple limite qui peut être augmentée dans le script).
/usr/local/bin/rtsp2http.py
#!/usr/bin/python # -*- coding: utf-8 -*- # Proxy RTSP vers HTTP v0.0.5 # (c) Puyb, AlexSoloex 2006, http://www.puyb.net # # Ce programme est distribue sous les termes la licence GPL v2 de la Free Software Fondation # This software is distributed under the terms of the Free Software Fondation's GPL v2 licence # Vous pouvez obtenir une copie de la licence a l'adresse : # You can find a copy of the licence at : # http://www.puyb.net/download/LICENCE-GPL2.txt # # changelog : # v0.0.5 - Ajout du Receiver Aknowledgment RTCP pour eviter la coupure du flux RTP, reduction des appels à gethostbyname, info de debuggage (detection des rupture de séquence RTP) # Par Antoine Emerit # v0.0.4 - Meilleur gestion du protocoles RTSP... Envois du paquet de reponse pour eviter les deconnexions # Par AlexSolex # v0.0.3 - reconnexion automatique du flux rtsp # v0.0.2 - correction de la taille des paquets UDP lus # - suppression de l'entete de 12 octets avant envoi du paquet en HTTP # v0.0.1 - version initiale import socket import sys import thread import time import struct import traceback import random _version="0.0.5" _server_string="rtsp2http proxy %s" % _version #------------------------------------------------------------------------------------------------- # une petire classe 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): 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 classe de client.. la meme 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 classe qui essaye de comprendre les reponses au requettes RTSP class basic_rtsp_socket(client_socket): def __init__(self, ip, port): client_socket.__init__(self, ip, port) self.cseq=1 self.RTCP_port=None def request(self, request): print '>>>\n', request % self.cseq self.send(request % self.cseq) self.cseq+=1 print "<<<\n" l=self.readline() print l (proto, code)=l.split(" ")[0:2] if proto[0:4]!="RTSP": raise Exception("Error: not a rtsp response", l) if code!="200": raise Exception("Error: rtsp error", l) l=self.readline() print l header={} while l!="" and l!=None: l2=l.split(": ") if len(l2)!=2: raise Exception("Incorrect response", l) #print " %s : %s"%(l2[0],l2[1]) header[l2[0]]=l2[1] if l2[0]=="Transport": prm_transport=l2[1].split(";") for prms in prm_transport:player try: #print prms key,val=prms.split("=") if key=="server_port": self.RTCP_port=int(val.split("-")[1]) break except: pass l=self.readline() print l if l==None: raise Exception("rtsp connection closed by foreign host", None) data=None print "---" if "Content-length" in header.keys(): data=read(int(header["Content-length"])) return (header, data) def char2bits(txt): bits = [] for char in txt: if type(char) == str:char = ord(char) for i in range(8): j = 2**(7-i) bits.append(int(char - j >= 0)) char = char % j print len(bits) print bits def bits2char(bits): char = 0 for b in range(8): if bits[7-b] == 1: char += 2**b return char #------------------------------------------------------------------------------------------------- def httpthread(http_conn, addr): data = http_conn.readline() print '------------------' print 'HTTP request from client' print '<<< ', data if not data: raise Exception("no data on http line", None) # on prend la première ligne (request, url, proto)=data.split(" ")[0:3] if request!="GET": print "ATTENTION : le player n'a pas envoyé de GET" if proto[0:4]!="HTTP": raise Exception("Error : not a http request", data) # on attend la fin de la requette # soit une ligne vide # un peu barbare... mais je suis sur qu'il n'y a rien à 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 Exception("http connection closed", None) # on repond le header reponse="""HTTP/1.0 200 OK Content-type: video/mp4 Cache-Control: no-cache """ print '>>> ', reponse http_conn.send(reponse)player print '------------------' reconnect=True #### #### while reconnect: # on se prepare a recevoir les connexion UDP rtsp_data = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) rtsp_data.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 100*1024*1024) # buffer de 100Mo, on devrait etre tranquille y compris en HD # preparation du control de flux rtsp_control = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) print 'Search for two free UDP port (RTP-RTCP)...' # on recherce un port libre entre 31330 et 31350 (port pair pour RTP et impair pour RTCP)player for rtp_port in range(31330, 31360, 2): try: print ("Try port %d" % rtp_port) rtsp_data.bind(("", rtp_port)) rtsp_control.bind(("",rtp_port+1)) print ("Bindind ports RTP-RTCP = %d-%d" % (rtp_port, rtp_port+1)) break; except: e = sys.exc_info()[0] print e print traceback.format_exc() rtsp_data.settimeout(1) print '------------------' print "Starting RTSP connection at %f" % time.clock() # on etablie la connexion rtsp rtsp_session=basic_rtsp_socket("mafreebox.freebox.fr", 554) (header, data)=rtsp_session.request("""SETUP rtsp://212.27.38.253%s RTSP/1.0 CSeq: %s Transport: RTP/AVP;unicast;client_port=%d-%d User-Agent: %s """ % (url, "%d", rtp_port, rtp_port+1,_version)) session=header["Session"] rtsp_control.connect(("mafreebox.freebox.fr",rtsp_session.RTCP_port)) (header, data)=rtsp_session.request("""PLAY rtsp://mafreebox.freebox.fr%s RTSP/1.0 CSeq: %s Session: %s Range: npt="now" User-Agent: %s """ % (url, "%d", session, _version)) # on repete tout au client http # on met des datas en memoire ok=True t=time.time() sequence_old=-1 cycle=0player sender_id=random.randint(0,16000000) source_id=0 mafreebox_addr=socket.gethostbyname("mafreebox.freebox.fr") print "Start playing..." while ok: if time.time()-t>5: sheader = struct.pack("!BBHLLLHHLLL", # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0b10000001,201,7, #header |V=2|P| RC | PT=SR=200 | length | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ sender_id, # | SSRC of sender | # +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ source_id, #report | SSRC_1 (SSRC of first source) | #block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0x00ffffff, # | fraction lost | cumulative number of packets lost | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ cycle,sequence_old, # | extended highest sequence number received | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0, # | interarrival jitter | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0, # | last SR (LSR) | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0 # | delay since last SR (DLSR) | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ) rtsp_control.send(sheader) t=time.time() # on lit 2k... Si le datagram est plus petit, on aura moins de donnees (FIXME : lire la MTU d'un paquet UDP) try: data, addr=rtsp_data.recvfrom(2048) except Exception, err: e = sys.exc_info()[0] print e print traceback.format_exc() ok=False print "RTSP TimeOut : retrying" if addr[0]==mafreebox_addr: try: sequence,timestamp,source_id = struct.unpack_from('!HLL', data[:12], 2) if sequence_old >=0 and sequence != sequence_old + 1: print("%d -> %d" % (sequence_old,sequence)) if sequence_old >= 0 and sequence <= sequence_old: if sequence_old > 65000 and sequence < 10: cycle += 1 else: print("%d -> %d : time travel !" % (sequence_old,sequence)) sequence_old = sequence http_conn.sendall(data[12:]) # on envoi la paylaod... C'est a dire, le datagram moins les 12 octets d'entete except Exception, err: print traceback.format_exc() ok=False # une exception... on arrete tout ! print "exception lors de l'envoi des données en http" reconnect=False else: print "not mafreebox.freebox.fr" # une des connexion a ete coupee, on ferme toutes les connexions... try:player print rtsp_session.request("""TEARDOWN rtsp://mafreebox.freebox.fr%s RTSP/1.0 CSeq: %s Session: %s User-Agent: rtsp2http v0.0.1 """ % (url, "%d", session)) print "HEADER TEARDOWN: \n%s"%header except: print "Exception on TEARDOWN, skiping" print "%s"%url rtsp_session.close() rtsp_control.close() rtsp_data.close() http_listen.close() http_conn.close() print "%s disconnected" % (addr, ) #------------------------------------------------------------------------------------------------- # Début du code # on met en place un serveur http print "=================================================================" print "Start server on port 8090" print "=================================================================" http_listen=server_socket(("", 8090)) while True: http_conn, addr = http_listen.accept() print '---------------------------------------------------------' print 'Client connection from IP: ', addr print '---------------------------------------------------------' thread.start_new_thread(httpthread, (http_conn, addr))
Pour lancer ce serveur en tâche de fond, j'ai écrit un wrappeur d'auto-restart et un script de démarrage système utilisant la commande
screen
:
/usr/local/bin/rtsp2http.sh
#!/bin/sh while true; do /usr/local/bin/rtsp2http.py done /etc/init.d/tv-toto.sh #!/bin/sh ### BEGIN INIT INFO # Provides: tvtoto # Required-Start: $local_fs $remote_fs $network # Required-Stop: $local_fs $remote_fs $network # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # X-Interactive: true # Short-Description: toto TV # Description: Start the toto TV ### END INIT INFO if [ "$1" = "start" ]; then screen -d -m -S tv_toto /usr/local/bin/rtsp2http.sh else pkill -f tv_toto fi
En lançant ce serveur par
/etc/init.d/tv-toto.sh
vous pouvez regarder une chaîne de TV en ouvant une URL de la forme http://tv.toto.com:8090/fbxtv_pub/stream?namespace=1&service=203&flavour=hd sous VLC (Ouvrir flux réseau).Serveur web
J'ai configuré un host virtuel sous Apache pour accèder au serveur à travers le port 80 standard du HTTP. Vous pouvez éventuellement rendre ce serveur accessible de l'extérieur mais attention à la sécurité.
/etc/apache2/sites-enabled/tv.toto.com.conf
#----------------------------------------------------------------- # Host virtuel #----------------------------------------------------------------- <VirtualHost *:80> ServerName tv.toto.com ServerAdmin webmaster@toto.com DocumentRoot /home/public/www/tv.toto.com/htdocs <Directory /home/public/www/tv.toto.com/htdocs> Require all granted Order deny,allow Allow from all </Directory> ErrorLog /var/log/apache2/tv.toto.com-error.log CustomLog /var/log/apache2/tv.toto.com-access.log vhost_combined DirectoryIndex index.php tv.html ProxyPass /fbxtv_pub/ http://tv.toto.com:8090/fbxtv_pub/ BrowserMatch "MSIE [2-6]" \ nokeepalive ssl-unclean-shutdown \ downgrade-1.0 force-response-1.0 BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown </VirtualHost>
Le host tv.toto.com et le chemin
/home/public/www/tv.toto.com/htdocs
doivent être adaptés à votre serveur. Ne pas oublier d'activer ce virtualhost sous Apache (a2ensite tv.toto.com
etservice apache2 restart
).Ce serveur web vListe des chaînes et playlist pour lecteur vidéoa aussi servir pour la partie programmation (ci-dessous).
Tout le contenu du dossier
htdocs
du host virtuel Apache est disponible dans une archive (il y a aussi un fichier pour la crontab). Inutile donc de recopier les scripts qui suivent, il suffit de décompresser le contenu de l'archive à la racine du site web (retirer ou fusionner les dossiershtdocs
).Liste des chaînes et playlist pour lecteur vidéo
Sur ce serveur nous allons pouvoir mettre une petite page PHP qui va nous fournir la liste des chaînes avec génération de playlist pour Windows media Player et VLC (ou autre lecteur compatible .m3u/.asx) :
/home/public/www/tv.toto.com/htdocs/chaines.php
<?php $main_channels = array("France 2", "France 3", "France 4", "France 5", "France Ô", "Arte", "RMC Découverte", "TMC", "Numéro 23", "La Chaîne Parlementaire", "NRJ 12", "C8", "BFM TV", "franceinfo", "Chérie 25", "L'Equipe", "CNews", "CStar"); #----------------------------------- if (!empty($_SERVER['SERVER_NAME'])) $base_url = ($_SERVER['HTTPS'] == 'on' ? 'https://' : 'http://') . $_SERVER['SERVER_NAME']; $base_dir = dirname(__FILE__); $channels = file_get_contents("$base_dir/playlist.m3u"); preg_match_all('|^#EXTINF:[0-9]+,([0-9]+) - (.*)$.*^(rtsp://mafreebox.freebox.fr/(.*))$|smU', $channels, $match); if (!isset($_REQUEST['channel'])) { header('Content-type: text/html; charset=UTF-8'); print "<p><a href='index.php'>Menu</a> <a href='chaines.php'>Chaînes principales</a> <a href='chaines.php?all'>Toutes les chaînes</a></p>"; if (!isset($_REQUEST['all'])) { $match[2] = array_filter($match[2], function($v) {global $main_channels; foreach ($main_channels as $c) {if (strpos($v, $c) !== FALSE) return true;} return false;}); } foreach ($match[2] as $key => $chan) { $chan = trim($chan); $lien = $match[3][$key]; $url = $match[4][$key]; $canal = $match[1][$key]; print "<a href='$url'><img src='images/download.gif' border=0></a>"; print "<a href='$lien'><img src='images/vlc.gif' border=0></a>"; print " $canal - <a href='chaines.php?channel=$key&type=.m3u'>$chan</a><br>\n"; } } else if (isset($_REQUEST['embeded'])) { $name = $match[2][$_REQUEST['channel']]; $url = "$base_url/" . $match[4][$_REQUEST['channel']]; header("Content-Type: video/x-mpegURL"); echo <<< EOF <?xml version="1.0" encoding="UTF-8"?> <playlist version="1" xmlns="http://xspf.org/ns/0/"> <trackList> <track> <title>К&#Liste des chaînes et playlist pour lecteur vidéo1072;нал 1</title> <location>$base_url/fbxtv_pub/stream?namespace=1&service=203&flavour=sd</location> </track> </trackList> </playlist> EOF; /* echo <<< EOF <html> <head> <title>$name</title> <link href="http://vjs.zencdn.net/4.8/video-js.css" rel="stylesheet"> <script src="http://vjs.zencdn.net/4.8/video.js"></script> </head> <body> <video id="MY_VIDEO" class="video-js vjs-default-skin" controls preload="no" width="640" height="264" data-setup="{}"> <source src="$url" type='video/mp4'> <p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p> </video> </body> </html> EOF; */ } else { header("Content-Type: audio/x-mpegurl"); header("Content-Disposition: inline; filename=\"tv.m3u\""); print "#EXTM3U\n"; print "#EXTINF:0," . $match[2][$_REQUEST[channel]] . "\n"; print "$base_url/" . $match[4][$_REQUEST['channel']] . "\n"; } ?>
Le fichier contenant la playlist FreeBox
/home/public/www/tv.toto.com/htdocs/playlist.m3u
sera mis à jour une fois par jour grace à la crontab :/home/public/www/www.toto.com/htdocs/blog/main/edit# cat /etc/cron.d/tv : 0 5 * * 0 root curl --silent --output /home/public/www/tv.toto.com/htdocs/playlist.m3u --location http://mafreebox.freebox.fr/freeboxtv/playlist.m3u && php /home/public/www/tv.toto.com/htdocs/chaines.php > /home/public/www/tv.toto.com/htdocs/chaines.html
Voilà, il n'y a plus qu'à ouvrir l'URL http://tv.toto.com/chaines.html pour avoir une liste clickable des flux TV :
Si vous regardez bien le fichier chaines.php vous verrez qu'il y a une liste de chaînes principales, à vous de la personaliser, mais rappelez-vous que toutes les chaînes ne sont pas accessible par flux IP (groupe TF1, groupe M6, chaînes payantes...).
Enregisteur personnel
Guide des programmes
Pour simplifier la programmation, il nous faut un guide des programmes. Ça tombe bien, il existe la solution compléte XSLTVGrid avec interface HTML/Javascript/XSL et les données des programmes sont gracieusement founies par Telerama à travers XMLTV France.
Aprés quelques modifications, notament pour générer une URL spécifique à notre PVR et retirer les chaînes non accessible en flux IP, nous allons mettre en place le système de programmation.
- décompressez la version modifiée de XSLTVGrid dans le dossier racine du host virtuel Apache ( dossier défini dans la configuration précédente /home/public/www/tv.toto.com/htdocs/ ). Cela doit créer un sous-dossier
guide
.- récupérer automatiquement la mise-à-jour des programmes par la crontab :
/etc/cron.d/tv
0 5 * * 0 root curl --silent --output /home/public/www/tv.toto.com/htdocs/guide/tv_source.xml http://xmltv.fr/guide/tvguide.xml && /home/public/www/tv.toto.com/htdocs/guide/clean_guide.sh 0 5 * * 0 root curl --silent --output /home/public/www/tv.toto.com/htdocs/playlist.m3u --location http://mafreebox.freebox.fr/freeboxtv/playlist.m3u && php /home/public/www/tv.toto.com/htdocs/chaines.php > /home/public/www/tv.toto.com/htdocs
On ne récupère les programmes qu'une fois par semaine (2 semaines d'avance). Lancez la mises-à-jour à la main puis accéder à la grille des programmes en ouvrant l'URL : http://tv.toto.com/guide/tv.html (note avec la configuration Apache précédente http://tv.toto.com/guide marche aussi).
Script de programmation
La programmation sera enregistrée en base de donnée qu'il faut créer au préalable sous MySQL :
-- -- Current Database: `pvr` -- CREATE DATABASE /*!32312 IF NOT EXISTS*/ `pvr` /*!40100 DEFAULT CHARACTER SET utf8mb4 */; USE `pvr`; -- -- Table structure for table `records` -- DROP TABLE IF EXISTS `records`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `records` ( `id` int(11) NOT NULL AUTO_INCREMENT, `channel` varchar(50) NOT NULL, `start` datetime NOT NULL, `stop` datetime NOT NULL, `title` varchar(255) DEFAULT NULL, `launched` tinyint(1) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `program` (`channel`,`start`,`stop`), KEY `launched` (`launched`,`start`) ) ENGINE=InnoDB AUTO_INCREMENT=536 DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; GRANT USAGE ON *.* TO 'pvr'@'localhost' IDENTIFIED BY 'pvr' GRANT ALL PRIVILEGES ON `pvr`.* TO 'pvr'@'localhost'
Et maintenant, nous avons le script de programmation et affichage des émissions déjà en base :
/home/public/www/tv.toto.com/htdocs/pvr.php
<?php header("Content-Type: text/html; charset=utf-8"); $base_dir = dirname(__FILE__); $videos_dir = "$base_dir/_Enregistrements"; $videos_url = '_Enregistrements'; if (!empty($_SERVER['SERVER_NAME'])) $base_url = ($_SERVER['HTTPS'] == 'on' ? 'https://' : 'http://') . $_SERVER['SERVER_NAME']; $db = new mysqli('localhost', 'pvr', 'duconlajoie', 'pvr'); //======================================================================================== // Launch records if (isset($argv) && $argv[1] == 'launch_records') { $base_url = $argv[2]; if ($to_launch = $db->query('select id,channel,start,TIMEDIFF(stop,start) as delay, TIMESTAMPDIFF(SECOND,start,stop) + 600 as length,title,IF(launched=0,\'\',\'X\') as onair from records where launched = false and DATE_SUB(start, INTERVAL 5 MINUTE) <= now()')) { $channels_urls = file_get_contents("$base_dir/playlist.m3u"---------------------); $url = array(); print "Records to launch :\n"; while ($info = $to_launch->fetch_array()) { $file_name = escapeshellcmd(str_replace(array(':', '\'', '?', '(', ')', '*', '&'), array('-',' ', ' ', ' ', ' ', ' ', ' '), "Le $info[start] pendant $info[delay] sur $info[channel] - $info[title]")); if (preg_match("|^#EXTINF:[0-9]+,[0-9]+ - .*$info[channel].*rtsp://mafreebox.freebox.fr/(fbxtv_pub/stream\?namespace=[0-9]+&service=[0-9]+&flavour=.+)$|misU", $channels_urls, $url)) { $cmd = "nohup curl --silent --max-time $info[length] --output '$videos_dir/$file_name.mpg' '$base_url/$url[1]' > /dev/null &"; print "Start record at $info[start] during $info[delay] on $info[channel] : $info[title]'\n$cmd\n"; system($cmd); print "\n"; if ($stmt = $db->prepare('update records set launched=true where id = ?')) if ($stmt->bind_param('i', $info['id'])) if ($stmt->execute()) print " Record '$info[id]' updated\n"; else print "Mysql error : (" . $stmt->errno . ")" . $stmt->error; else print "Mysql error : (" . $stmt->errno . ") " . $stmt->error; else print "Mysql error : (" . $db->errno . ") " . $db->error; } else { print "No match at $info[start] during $info[length] on $info[channel] : $info[title]'\n"; } } } else print "Mysql error : (" . $db->errno . ") " . $db->error; //======================================================================================== // Add a record in the database } else if (isset($_REQUEST['cmd']) && $_REQUEST['cmd'] == 'add') { $db->set_charset('latin1'); if ($stmt = $db->prepare("insert into records (channel, start, stop, title, launched) values (?, STR_TO_DATE(?,'%Y%m%d%H%i%s'), STR_TO_DATE(?,'%Y%m%d%H%i%s'), ?, 0)")) if ($stmt->bind_param('ssss', $_REQUEST['channel'], $_REQUEST['start'], $_REQUEST['stop'], $_REQUEST['title'])) if ($stmt->execute()) print "Record inserted\n"; else print "Mysql error : (" . $stmt->errno . ") " . $stmt->error; else --------------------- print "Mysql error : (" . $stmt->errno . ") " . $stmt->error; else print "Mysql error : (" . $db->errno . ") " . $db->error; $db->set_charset('utf8'); //======================================================================================== // Remove a record from the database } else if (isset($_REQUEST['cmd']) && $_REQUEST['cmd'] == 'del') { if (is_numeric($_REQUEST['id']) && ($record = $db->query("select id,channel,start,timediff(stop,start) as delay,title,if(launched=0,'','X') as onair from records where id = $_REQUEST[id]"))) { $info = $record->fetch_array(); $file_name = str_replace(array(':', '\'', '?', '(', ')', '*', '&'), array('-', ' ', ' ', ' ', ' ', ' ', ' '), "Le $info[start] pendant $info[delay] sur $info[channel] - $info[title]"); } else $file_name = ''; if ($stmt = $db->prepare('delete from records where id = ?')) if ($stmt->bind_param('i', $_REQUEST['id'])) if ($stmt->execute()) { if (!empty($file_name)) unlink("$videos_dir/$file_name.mpg"); header("Location: pvr.php"); print "<!-- \n"; print "Record '$_REQUEST[id]' removed\n"; print "File '$videos_dir/$file_name.mpg' removed\n"; print " -->\n"; } else print "Mysql error : (" . $stmt->errno . ") " . $stmt->error; else print "Mysql error : (" . $stmt->errno . ") " . $stmt->error; else print "Mysql error : (" . $db->errno . ") " . $db->error; //======================================================================================== // Video streaming } else if (isset($_REQUEST['asx'])) { $film = "$base_url/$videos_url/" . rawurlencode($_REQUEST['asx']); header("Content-type: video/x-ms-asf"); echo <<< EOF <asx version="3.0"> <title>$film</title> <entry> <title>$film</title> <ref href="$film"/> </entry> </asx> EOF; } else if (isset($_REQUEST['m3u'])) { $film = "$base_url/$videos_url/" . rawurlencode($_REQUEST['m3u']); header("Content-type: application/vnd.apple.mpegurl"); echo <<< EOF #EXTM3U #EXTVLCOPT:freetype-rel-fontsize=16 $film EOF; //======================================================================================== // List all records in the database } else { $records = $db->query('set lc_time_names=\'fr_FR\''); $records = $db->query('select id,channel,start,date_format(start, \'%W %e %M %Y %H:%i:%s\') as debut, timediff(stop,start) as delay,title,if(launched=0,\'\',\'X\') as onair from records order by start,channel'); if (!isset($argv)) { print "<html>\n<body>\n"; print "<p><a href='index.php'>Menu</a> <a href='pvr.php'>Rafraichir</a></p>\n"; print "<table border=1>\n"; print "<tr><th>Début<th>Durée<th>Chaîne<th>Titre<th>Enregistré<br>(ou en cours)\n"; while ($info = $records->fetch_array()) { $file_name = htmlspecialchars(str_replace(array(':', '\'', '?', '(', ')', '*', '&'), array('-',' ', ' ', ' ', ' ', ' ', ' '), "Le $info[start] pendant $info[delay] sur $info[channel] - $info[title].mpg")); print "<tr><td>$info[debut]<td>$info[delay]<td>$info[channel]<td><a href='$videos_url/$file_name'><img src='images/download.gif' border=0 title='Télécharger'></a> <a href='pvr.php?m3u=$file_name'><img src='images/vlc.gif' border=0 title='Voir'></a><a href='pvr.php?m3u=$file_name'>$info[title]</a><td align=center>" . (!empty($info['onair']) ? "<img src='images/ok.png' border=0>" : "") . "<td>" . ( empty($info['onair']) ? "<a href='pvr.php?cmd=del&id=$info[id]'>" : "<a href='pvr.php?cmd=del&id=$info[id]' onclick=\"if (confirm('L\'enregistrement va être supprimé.')) return true; else return false;\">" ) . "<img src='images/delete.png' border=0 title='Supprimer programmation et enregistrement'></a>\n"; } print "</table>\n"; print "<pre>Enregistrements en cours :\n\n"; system("pgrep --full --list-full curl"); print "</pre>\n"; print "</body></html>\n"; } else { while ($info = $records->fetch_array()) { print "$info[channel]\t$info[start]\t$info[delay]\t$info[title]\t$info[onair]\n"; } system("ps xwau | grep curl"); } } ?>
Ce script est utilisable en ligne de commande et en tant que page PHP. Sans paramètre il affiche les émissions déjà programmées en base avec un status 'enregistré/en cours' ou 'à venir'.
Maintenant que ce script est dans notre host virtuel, on peut accéder à la page web avec l'URL http://tv.toto.com/pvr.php :
...
Il est nécessaire de modifier les variables
$videos_dir
pour définir le dossier qui contiendra les enregistrements vidéos, ce dossier devra être modifiable par l'utilisateur du serveur Apache (www-data sous Debian).Le bas de la page affiche aussi les processus d'enregistrements en cours.
L'URL d'enregistrement est de la forme http://tv.toto.com/pvr.php?cmd=add&channel=France%204&start=20180604202500%20%2B0200&stop=20180604205000%20%2B0200&title=Une%20saison%20au%20zoo, et elle est appelée depuis le guide des programmes.
Pour que les enregistrement se fassent réellement il faut appeler le script pvr.ph depuis la crontab (toutes les minutes) avec le paramètre
launch_records
:
/etc/cron.d/tv
* * * * * nobody /usr/bin/php /home/public/www/tv.kozodo.com/htdocs/pvr.php launch_records 'http://tv.toto.com' > /dev/null 2>&1
Page de garde
Enfin pour finir une petit page HTML avec des liens vers les différentes pages utiles :
index.php
<html> <body> <p><a href="/chaines.html">Chaînes de TV</a></p> <p><a href="/guide/tv.html">Guide des programmes TV</a></p> <p><a href="/pvr.php">Programmation</a></p> </body> </html>
Améliorations possibles
Ce système est vraiment minimaliste, et il faudrait y appliquer un certain nombre d'évolutions :
- sécuriser le code : anti SQL/PHP/commande injection ...
- sécuriser l'accès : gestion d'utilisateur, filtrage par IP ? ...
- guide des programmes : icônes des chaînes, images des émissions...
- programmation : par dates saisie manuellement, gestion des séries, automatique par mot-clé/type d'émission/présentateur/..., suppression automatique (durée de rétention, après visualisation, émission équivalente...), alertes par email...
- interface : aspect/interactivité, adapté au mobile/tablette...
Bonne programmation et bonne visualisation.
Antoine