diff --git a/tv/radio/config.py b/tv/radio/config.py new file mode 100644 index 00000000..87e89bce --- /dev/null +++ b/tv/radio/config.py @@ -0,0 +1,33 @@ +# -*- coding: utf8 -*- +import os, random, collections + +def dir(path): + l=collections.deque(["%s%s" % (path,i) for i in os.listdir(path)]) + l.rotate(random.randrange(0,len(l))) + return l +multicast={ +'Radio': { + 'Armitunes': ('armitunes','239.231.140.162','1234',['http://149.255.33.76:8004/','http://95.31.11.136:9010/','http://95.31.3.225:9010/','http://hanyo.dyndns-server.com:9078/']), + 'Radio Classique': ('classique','239.231.140.163','1234',['http://broadcast.infomaniak.net:80/radioclassique-high.mp3']), + 'France Inter': ('inter','239.231.140.164','1234',['http://mp3.live.tv-radio.com/franceinter/all/franceinterhautdebit.mp3']), + 'France Info': ('info','239.231.140.165','1234',['http://mp3.live.tv-radio.com/franceinfo/all/franceinfo-32k.mp3']), + 'Webradio Chibre': ('chibre','239.231.140.166','1234',['http://webradio.crans.org:8000/chibre.mp3']), + 'Webradio Clubbing': ('clubbing','239.231.140.167','1234',['http://webradio.crans.org:8000/clubbing.mp3']), + 'Webradio Rock': ('rock','239.231.140.168','1234',['http://webradio.crans.org:8000/rock.mp3']), + 'I.ACTIVE DANCE': ('iactive','239.231.140.170', '1234', ['http://serveur.wanastream.com:48700/']), + 'Skyrock': ('skyrock', '239.231.140.171', '1234', ['http://95.81.146.6/3665/sky_122353.mp3']), + 'Rire et Chanson': ('rireetchanson', '239.231.140.172', '1234', ['http://95.81.146.2/rire_et_chansons/all/rir_124629.mp3']), + 'Europe 1': ('europe1', '239.231.140.173', '1234', ['http://vipicecast.yacast.net/europe1.mp3']), + 'Chérie FM': ('cherie_fm', '239.231.140.174', '1234', ['http://95.81.146.2/cherie_fm/all/che_124310.mp3']), + 'France Culture': ('culture', '239.231.140.175', '1234', ['http://95.81.147.3/franceculture/all/franceculturehautdebit.mp3']), + 'BFM': ('bfm', '239.231.140.176', '1234', ['http://vipicecast.yacast.net/bfm.mp3']), + 'France Musique': ('musique', '239.231.140.177', '1234', ['http://95.81.147.3/francemusique/all/francemusiquehautdebit.mp3']), + 'Fun Radio': ('funradio', '239.231.140.178', '1234', ['http://streaming.radio.funradio.fr/fun-1-44-128.mp3']), + 'Nostalgie': ('nostalgie', '239.231.140.179', '1234', ['http://95.81.146.2/nostalgie/all/nos_113812.mp3']), + 'le mouv\'': ('lemouv', '239.231.140.180', '1234', ['http://95.81.147.3/lemouv/all/lemouvhautdebit.mp3']), + 'NRJ': ('nrj', '239.231.140.181', '1234', ['http://95.81.146.2/nrj/all/nrj_113225.mp3']), + + + }, +} + diff --git a/tv/radio/dns.py b/tv/radio/dns.py new file mode 100644 index 00000000..77400849 --- /dev/null +++ b/tv/radio/dns.py @@ -0,0 +1,37 @@ +import socket +import time + +dns={} +def ip2name(ip): + if dns.get(ip, None) and time.time() - dns[ip][1] < 600: + return dns[ip][0] + try: + ret = timeout(socket.gethostbyaddr, args=[ip], default=(ip,), timeout_duration=1)[0] + except herror: + ret = ip + dns[ip]=(ret, time.time()) + return ret + +def timeout(func, args=(), kwargs={}, timeout_duration=10, default=None): + """This function will spawn a thread and run the given function + using the args, kwargs and return the given default value if the + timeout_duration is exceeded. + """ + import threading + class InterruptableThread(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.result = default + def run(self): + try: + self.result = func(*args, **kwargs) + except: + self.result = default + it = InterruptableThread() + it.start() + it.join(timeout_duration) + if it.isAlive(): + + return it.result + else: + return it.result diff --git a/tv/radio/igmp.init b/tv/radio/igmp.init new file mode 100755 index 00000000..94df2efa --- /dev/null +++ b/tv/radio/igmp.init @@ -0,0 +1,36 @@ +#!/bin/bash +### BEGIN INIT INFO +# Provides: igmp_tracker +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: igmp subscribe tracker +### END INIT INFO + +DAEMON="/usr/bin/python" +ARGS="/usr/scripts/tv/radio/igmp.py" +PIDFILE="/var/run/igmp.pid" +USER="root" + +case "$1" in + start) + echo "Starting server" + /sbin/start-stop-daemon --start --pidfile $PIDFILE \ + --user $USER --group $USER \ + -b --make-pidfile \ + --chuid $USER \ + --exec $DAEMON $ARGS + ;; + stop) + echo "Stopping server" + /sbin/start-stop-daemon --stop --pidfile $PIDFILE --verbose + ;; + *) + echo "Usage: $0 {start|stop}" + exit 1 + ;; +esac + +exit 0 + diff --git a/tv/radio/igmp.py b/tv/radio/igmp.py new file mode 100755 index 00000000..917990fa --- /dev/null +++ b/tv/radio/igmp.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python +import socket +from struct import * +import datetime +import pcapy +import sys +import time +from dns import ip2name + +import config + +group_to_vlm={} + +for g in config.multicast.values(): + for tuple in g.values(): + group_to_vlm[tuple[1]]=group_to_vlm.get(tuple[1], []) + [tuple[0]] + +def run_vlc(cmd): + try: + s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(('127.0.0.1', 4212)) + ret=s.recv(65536) + s.send('admin\n') + #~ print cmd + s.send(cmd+'\n') + s.settimeout(0.3) + + try: + while True: + ret+=s.recv(65536) + except socket.timeout: + pass + s.close() + return ret + except socket.error: + return "" + +def stop(group): + if group_to_vlm.get(group, None): + cmd='\n'.join('control %s stop' % name for name in group_to_vlm[group]) + run_vlc(cmd) + +def play(group): + if group_to_vlm.get(group, None): + cmd='\n'.join('control %s play' % name for name in group_to_vlm[group]) + run_vlc(cmd) + +def playing(group): + ret=True + if group_to_vlm.get(group, None): + for name in group_to_vlm[group]: + ret=ret and 'playing' in run_vlc('show %s' % name) + return ret + + +class IgmpTable(object): + + def __init__(self, dev, ignore_src=[], debug=0, resolve_dns=False, logpath=None): + self.CLEAN_FREQ=10 + self.MC_REPORT=300 + self.CLEAN_LAST=0 + self.STREAM_LAST=time.time() + self.table={} + self.debug=debug + self.counter={} + self.dev=dev + self.ignore_src=ignore_src + self.resolve_dns=resolve_dns + self.logpath=logpath + + self.sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IGMP) + self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + + def run(self): + cap = pcapy.open_live(self.dev , 65536 , 1 , self.CLEAN_FREQ * 1000) + cap.setfilter('igmp') + + #start sniffing packets + try: + while(True) : + try: + (header, packet) = cap.next() + tuple=self.parse_packet(packet) + if tuple and tuple[0][0] not in self.ignore_src: + # type: Membership Query (17), Membership Report (IGMPv1: 18, IGMPv2: 22, IGMPv3: 34), Leave Group (23) + ((src, dst), type, group) = tuple + if type in [18, 22, 34]: + #print "%s report %s" % (src, group) + self.report(group, src) + elif type == 23: + #print "%s leaving %s" % (src, group) + self.leave(group, src) + #else: + # print type + self.clean() + self.clean_stream() + except socket.timeout: + pass + except (KeyboardInterrupt,): + exit(0) + + def query(self, group): + igmph_length = 8 + igmp_type = 17 + max_resp_time = 10 #self.CLEAN_FREQ * 5 + igmp_group = ''.join(str(chr(int(i))) for i in group.split('.')) + igmp = pack('!BBBB' , igmp_type, max_resp_time, 0, 0) + igmp_group + + def carry_around_add(a, b): + c = a + b + return (c & 0xffff) + (c >> 16) + + def checksum(msg): + s = 0 + for i in range(0, len(msg), 2): + w = ord(msg[i]) + (ord(msg[i+1]) << 8) + s = carry_around_add(s, w) + return ~s & 0xffff + + igmp=igmp[0:2] + pack('H', checksum(igmp)) + igmp[4:] + self.sock.sendto(igmp, (group, 0)) + + def leave(self, group, src): + tp=time.time() + if self.table.get(group, None) and self.table[group].get(src, None) and tp - self.table[group][src] < self.MC_REPORT: + if len([ip for ip in self.table[group].keys() if tp - self.table[group][ip] < self.MC_REPORT])<=1: + self.CLEAN_LAST=time.time() - 5 + self.table[group][src]=0 + #self.query(group) #Inutile, mcproxy le fait + if self.debug>0: + self.print_table() + self.log() + else: + if not self.table.get(group, None): + self.table[group]={} + self.counter[group]=0 + self.query(group) + + #function to parse a packet + def parse_packet(self, packet) : + + #parse ethernet header + eth_length = 14 + + eth_header = packet[:eth_length] + eth = unpack('!6s6sH' , eth_header) + eth_protocol = socket.ntohs(eth[2]) + + #Parse IP packets, IP Protocol number = 8 + if eth_protocol == 8 : + #Parse IP header + #take first 20 characters for the ip header + ip_header = packet[eth_length:20+eth_length] + + #now unpack them :) + iph = unpack('!BBHHHBBH4s4s' , ip_header) + + version_ihl = iph[0] + version = version_ihl >> 4 + ihl = version_ihl & 0xF + + iph_length = ihl * 4 + + ttl = iph[5] + protocol = iph[6] + s_addr = socket.inet_ntoa(iph[8]); + d_addr = socket.inet_ntoa(iph[9]); + + + #IGMP Packets + if protocol == 2 : + u = iph_length + eth_length + igmph_length = 8 + igmp_header = packet[u:u+4] + igmp_group = socket.inet_ntoa(packet[u+4:u+8]) + igmph = unpack('!BBH' , igmp_header) + + igmp_type = igmph[0] + max_resp_time = igmph[1] + checksum = igmph[2] + + return ((s_addr, d_addr), igmp_type, igmp_group) + + #h_size = eth_length + iph_length + igmph_length + #data_size = len(packet) - h_size + ##get data from the packet + #data = packet[h_size:] + + def report(self, group, ip): + try: + if not self.table[group].get(ip, None): + self.counter[group]+=1 + play(group) + self.table[group][ip]=time.time() + except KeyError: + self.table[group]={ip:time.time()} + self.counter[group]=1 + play(group) + + def clean(self): + if time.time() - self.CLEAN_LAST < self.CLEAN_FREQ: + return + for (group, ips) in self.table.items(): + for (ip, last) in ips.items(): + if time.time() - last > self.MC_REPORT: + del(self.table[group][ip]) + self.counter[group]-=1 + if not self.table[group]: + del(self.table[group]) + del(self.counter[group]) + stop(group) + elif not playing(group): + play(group) + self.CLEAN_LAST=time.time() + if self.debug > 0: + self.print_table() + self.log() + + def clean_stream(self): + if time.time() - self.STREAM_LAST < self.MC_REPORT: + return + for group in group_to_vlm.keys(): + if not self.counter.get(group, None): + stop(group) + self.STREAM_LAST=time.time() + + + def print_table(self): + print self.summarise() + + def log(self): + if self.logpath: + f=open(self.logpath, 'w') + f.write(self.summarise()) + f.close() + + def summarise(self): + l=self.table.items() + l.sort() + tp=time.time() + def resolve(ip): + if self.resolve_dns: + return ip2name(ip) + else: + return ip + ret="" + for (group, ips) in l: + ret+= "%s: %s\n" % (resolve(group), ', '.join(["%s (%ss)" % (resolve(k[0]), int(tp - k[1])) for k in ips.items()]) if len(ips) < 10 else "%s clients" % len(ips)) + ret+= "-"*80 + "\n" + return ret + + + +if __name__ == "__main__": + IgmpTable('eth0', ignore_src=['138.231.136.88', '0.0.0.0'], logpath='/var/log/igmp.log').run() diff --git a/tv/radio/radio.init b/tv/radio/radio.init new file mode 100755 index 00000000..eaa27769 --- /dev/null +++ b/tv/radio/radio.init @@ -0,0 +1,80 @@ +#!/bin/bash +### BEGIN INIT INFO +# Provides: radio +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: network radio +### END INIT INFO + +DIR="/usr/scripts/tv/radio/" +PIDFILE="/tmp/multicast.pid" +TMP="/tmp/" +sap_gen(){ + PREV="`md5sum /etc/sap.cfg`" + ${DIR}sap.py > /etc/sap.cfg + NEXT="`md5sum /etc/sap.cfg`" + if [ "$PREV" != "$NEXT" ]; then + /etc/init.d/minisapserver force-reload >/dev/null + fi; +} +vlc_start() { + if [ -f $PIDFILE ]; then + pid=`cat $PIDFILE` + if [ `ps -p $pid | wc -l` -eq 2 ]; then + exit 0 + fi + fi + ${DIR}vlm.py > ${TMP}vlm.vlm + /usr/bin/sudo -u www-data cvlc --extraintf telnet --ttl 12 --vlm-conf ${TMP}vlm.vlm --pidfile ${PIDFILE} --daemon >/dev/null +} + + +vlc_stop(){ + if [ -f $PIDFILE ]; then + pid=`cat $PIDFILE` + kill $pid + fi +} + +vlc_status(){ + if [ -f $PIDFILE ]; then + pid=`cat $PIDFILE` + if [ `ps -p $pid | wc -l` -eq 2 ]; then + echo "VLC is running." + exit 0 + fi + fi + echo "VLC is NOT running." +} + +usage(){ + echo "Usage: $0 {start|stop|restart}" + exit 1 +} + +case $1 in + start) + sap_gen; + vlc_start; + ;; + stop) + vlc_stop; + ;; + restart) + sap_gen; + for i in `seq 10`; do + vlc_stop; + sleep 1; + if [ ! -f $PIDFILE ]; then + break + fi + echo -n "." + done + vlc_start; + ;; + *) + usage; + ;; +esac diff --git a/tv/radio/sap.py b/tv/radio/sap.py new file mode 100755 index 00000000..660b4b96 --- /dev/null +++ b/tv/radio/sap.py @@ -0,0 +1,21 @@ +#!/usr/bin/python +from config import * +import socket + +print("""[global] +sap_delay=9 +interface=eth0 +#ttl=1 +""") + +for group in multicast.keys(): + for name in multicast[group].keys(): + (chan,dst,port,source)=multicast[group][name] + print("""[program] +name=%s +address=%s +port=%s +playlist_group=%s +user=crans +machine=%s +""" % (name,dst,port,group, socket.gethostname())) diff --git a/tv/radio/vlm.py b/tv/radio/vlm.py new file mode 100755 index 00000000..c8fef351 --- /dev/null +++ b/tv/radio/vlm.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf8 -*- +from config import * + +i=0 +for group in multicast.keys(): + for (name,dst,port,sources) in multicast[group].values(): + #name="channel%s" % i + print('new %s broadcast enabled loop' % name) + for source in sources: + print('setup %s input "%s"' % (name,source)) + print('setup %s output #udp{mux=ts,dst=%s:%s}' % (name,dst,port)) + print('setup %s option network-caching=50' % name) + print('setup %s option sout-all' % name) + print('setup %s option sout-keep' % name) + #print('control %s play' % name) + i+=1