d69ba52001-09-03Martin Nilsson // This is a roxen module. Copyright © 2001, Roxen IS.
650fea2001-04-10Per Hedbor inherit "module";
d69ba52001-09-03Martin Nilsson constant cvs_version="$Id: icecast.pike,v 1.9 2001/09/03 18:16:56 nilsson Exp $";
650fea2001-04-10Per Hedbor constant thread_safe=1;
b779eb2001-04-10Per Hedbor #define BSIZE 8192 #define METAINTERVAL 4192
650fea2001-04-10Per Hedbor #include <module.h> #include <roxen.h> #include <stat.h> #include <request_trace.h> //<locale-token project="mod_icecast">LOCALE</locale-token> #define _(X,Y) _DEF_LOCALE("mod_icecast",X,Y) constant module_type = MODULE_LOCATION; LocaleString module_name = _(0,"Icecast: Server");
4f76ba2001-04-10Per Hedbor LocaleString module_doc = _(0, "Supports the ICY and Audio-cast protocols " "for streaming MPEG sound. Relies on other " "modules for the actual mpeg streams." );
650fea2001-04-10Per Hedbor constant module_unique = 0; class Playlist { inherit RoxenModule; Stdio.File next_file(); string pl_name(); mapping metadata(); void add_md_callback( function(mapping:void) f ); void remove_md_callback( function(mapping:void) f ); }; class MPEGStream( Playlist playlist ) { array(function) callbacks = ({});
c0c9b92001-04-11Per Hedbor  int bitrate; // bits/second int stream_start=realtime(), stream_position; // µSeconds
650fea2001-04-10Per Hedbor  Stdio.File fd;
b779eb2001-04-10Per Hedbor  string status() { return ""+playlist->status()+"<br />"+
c0c9b92001-04-11Per Hedbor  sprintf( "Stream position: %.1fs Bitrate: %dKbit/sec", stream_position/1000000.0,
b779eb2001-04-10Per Hedbor  bitrate/1000 ); }
c0c9b92001-04-11Per Hedbor 
650fea2001-04-10Per Hedbor  void add_callback( function callback ) { callbacks += ({ callback }); } void remove_callback( function callback ) { callbacks -= ({ callback }); } void call_callbacks( mixed ... args ) { foreach( callbacks, function f )
b779eb2001-04-10Per Hedbor  if( mixed e = catch( f(@args) ) ) { werror(describe_backtrace( e ) );
6877ce2001-04-11Per Hedbor  remove_callback( f );
b779eb2001-04-10Per Hedbor  }
650fea2001-04-10Per Hedbor  }
c0c9b92001-04-11Per Hedbor  static int last_hrtime, base_time;
650fea2001-04-10Per Hedbor  int realtime() {
c0c9b92001-04-11Per Hedbor  if(!last_hrtime) { last_hrtime = gethrtime(); base_time = 0; } else { int nt = gethrtime(); base_time += nt-last_hrtime; last_hrtime = nt; } return base_time;
650fea2001-04-10Per Hedbor  }
b779eb2001-04-10Per Hedbor  void destroy() { catch(fd->set_blocking()); catch(fd->close()); }
650fea2001-04-10Per Hedbor  void feeder_thread( ) {
6877ce2001-04-11Per Hedbor  while( sizeof(callbacks) &&
c0c9b92001-04-11Per Hedbor  ((realtime()-stream_start) > stream_position) )
650fea2001-04-10Per Hedbor  { string frame = get_frame(); while( !frame ) {
b779eb2001-04-10Per Hedbor  fd->set_blocking();
650fea2001-04-10Per Hedbor  fd->close(); fd = playlist->next_file();
b779eb2001-04-10Per Hedbor  buffer = 0;
650fea2001-04-10Per Hedbor  bpos = 0; frame = get_frame(); }
c0c9b92001-04-11Per Hedbor  // Actually, this is supposed to be quite constant, but not all // frames are the same size. stream_position += strlen(frame)*8000000 / bitrate;
650fea2001-04-10Per Hedbor  call_callbacks( frame );
6877ce2001-04-11Per Hedbor  } if(!sizeof(callbacks)) { stream_position = 0;
c0c9b92001-04-11Per Hedbor  stream_start = realtime();
6877ce2001-04-11Per Hedbor  }
650fea2001-04-10Per Hedbor  call_out( feeder_thread, 0.02 ); } void start() { feeder_thread( ); // not actually a thread right now. } // Low level code. static string buffer; static int bpos; static string|int getbytes( int n, int|void s ) { if( !fd ) fd = playlist->next_file();
6877ce2001-04-11Per Hedbor  if( !fd ) return s?0:-1;
b779eb2001-04-10Per Hedbor  if( !buffer || !strlen(buffer) ) { bpos = 0;
650fea2001-04-10Per Hedbor  buffer = fd->read( BSIZE );
b779eb2001-04-10Per Hedbor  } if( !strlen(buffer) ) return s?0:-1;
650fea2001-04-10Per Hedbor  if( s ) { if( strlen(buffer) - bpos > n ) { string d = buffer[bpos..bpos+n-1]; buffer = buffer[bpos+n..]; bpos=0; return d; } else { buffer = buffer[bpos..]; bpos=0; string t = fd->read( BSIZE ); if( !t || !strlen(t) ) return -1; buffer+=t; return getbytes(n,1); } } int res=0; while( n-- ) { res<<=8; res|=buffer[ bpos++ ]; if( bpos == strlen(buffer) ) { bpos = 0; buffer = fd->read( BSIZE ); if( !buffer || !strlen( buffer ) ) return -1; } } return res; } static int rate_of(int r) { switch(r) { case 0: return 44100; case 1: return 48000; case 2: return 32000; default:return 44100; } } static array(array(int)) bitrates = ({ ({0,32,64,96,128,160,192,224,256,288,320,352,384,416,448}), ({0,32,48,56,64,80,96,112,128,160,192,224,256,320,384}), ({0,32,40,48,56,64,80,96,112,128,160,192,224,256,320}), }); static string get_frame() { string data; /* Find header */ int trate = 0; int patt = 0; int by, p=0, sw=0; while( (by = getbytes( 1 )) > 0 ) { patt <<= 8; patt |= by; p++; if( (patt & 0xfff0) == 0xfff0 ) { int srate, channels, layer, ID, pad, blen; int header = ((patt&0xffff)<<16); if( (by = getbytes( 2 )) < 0 ) break; header |= by; string data = sprintf("%4c",header); patt=0; header <<= 12; int getbits( int n ) { int res = 0; while( n-- ) { res <<= 1; if( header&(1<<31) ) res |= 1; header<<=1; } return res; }; ID = getbits(1); // version if(!ID) /* not MPEG1 */ continue; layer = (4-getbits(2)); header<<=1; /* getbits(1); // error protection */ bitrate = getbits(4); srate = getbits(2); if((layer>3) || (layer<2) || (bitrate>14) || (srate>2)) continue; pad = getbits(1); bitrate = bitrates[ layer-1 ][ bitrate ] * 1000; srate = rate_of( srate ); switch( layer ) { case 1: blen = (int)(12 * bitrate / (float)srate + (pad?4:0))-4; break; case 2: case 3: blen = (int)(144 * bitrate / (float)srate + pad )-4; break; }
b779eb2001-04-10Per Hedbor  string q = getbytes( blen,1 ); if(!q) return 0; return data + q;
650fea2001-04-10Per Hedbor  } } return 0; } } class Location( string location, string initial, MPEGStream stream,
c0c9b92001-04-11Per Hedbor  int max_connections, string url, string name )
650fea2001-04-10Per Hedbor  { int accessed, denied; int connections;
b779eb2001-04-10Per Hedbor  int total_time, max_time, successful;
650fea2001-04-10Per Hedbor  array(Connection) conn = ({}); mapping handle( RequestID id ) {
b779eb2001-04-10Per Hedbor  NOCACHE();
650fea2001-04-10Per Hedbor  accessed++; if( connections == max_connections ) { denied++; return Roxen.http_string_answer( "Too many listeners\n" ); }
6877ce2001-04-11Per Hedbor  if( !stream->fd ) stream->fd = stream->playlist->next_file();
b779eb2001-04-10Per Hedbor  mapping meta = stream->playlist->metadata(); if( !meta ) { denied++; return Roxen.http_string_answer( "Too early\n" ); } connections++; int use_metadata; string i, metahd="";
9c812a2001-04-10Per Hedbor  string protocol = "ICY";
650fea2001-04-10Per Hedbor 
b779eb2001-04-10Per Hedbor  werror("%O\n", id->request_headers );
d3cea92001-05-16Per Hedbor  if( 0 && id->request_headers[ "icy-metadata" ] )
b779eb2001-04-10Per Hedbor  {
d3cea92001-05-16Per Hedbor  use_metadata = (int)id->request_headers[ "icy-metadata" ]; if( use_metadata ) metahd = "icy-metaint:"+METAINTERVAL+"\r\n";
b779eb2001-04-10Per Hedbor  } if( id->request_headers[ "x-audiocast-udpport" ] ) { protocol = "AudioCast"; i = ("HTTP/1.0 200 OK\r\n" "Server: "+roxen.version()+"\r\n" "Content-type: audio/mpeg\r\n" "x-audiocast-gengre:"+(meta->gengre||"unknown")+"\r\n"
c0c9b92001-04-11Per Hedbor  +((meta->url||url)?"x-audiocast-url:"+(meta->url||url)+"\r\n":"")+ "x-audiocast-name:"+(meta->name||name||meta->path)+"\r\n"
b779eb2001-04-10Per Hedbor  "x-audiocast-streamid:1\r\n"+metahd+ "x-audiocast-public:1\r\n" "x-audiocast-bitrate:"+(stream->bitrate/1000)+"\r\n" "x-audiocast-description:Served by Roxen\r\n" "\r\n" ); } else { i = ("ICY 200 OK\r\n" "Server: "+roxen.version()+"\r\n" "Content-type: audio/mpeg\r\n" "icy-notice1:This stream requires a shoutcast compatible player.\r\n" "icy-notice2:Roxen mod_mp3\r\n"+metahd+
c0c9b92001-04-11Per Hedbor  "icy-name:"+(meta->name||name||meta->path)+"\r\n"
b779eb2001-04-10Per Hedbor  "icy-gengre:"+(meta->gengre||"unknown")+"\r\n"
c0c9b92001-04-11Per Hedbor  +((meta->url||url)?"icy-url:"+(meta->url||url)+"\r\n":"")+
b779eb2001-04-10Per Hedbor  "icy-pub:1\r\n" "icy-br:"+(stream->bitrate/1000)+"\r\n" "\r\n" ); }
650fea2001-04-10Per Hedbor  if( initial ) i += initial;
b779eb2001-04-10Per Hedbor  conn += ({ Connection( id->my_fd, i,protocol,use_metadata,
d3cea92001-05-16Per Hedbor  stream, this_object(),
650fea2001-04-10Per Hedbor  lambda( Connection c ){
b779eb2001-04-10Per Hedbor  int pt = time()-c->connected;
8626872001-04-17Per Hedbor  conn -= ({ c });
b779eb2001-04-10Per Hedbor  total_time += pt; if( pt > max_time ) max_time = pt; successful++;
650fea2001-04-10Per Hedbor  connections--; } ) }); return Roxen.http_pipe_in_progress( ); }
b779eb2001-04-10Per Hedbor  string format_time( int t ) { if( t > 60*60 ) return sprintf( "%d:%02d:%02d", t/3600, (t/60)%60, t%60 ); return sprintf( "%2d:%02d", (t/60)%60, t%60 ); } string avg_listen_time() { int tt = total_time; int n = successful; foreach( conn, Connection c ) { n++; tt += time()-c->connected; } int t = tt / (n||1); return format_time( t ); } string longest_listen_time() { foreach( conn, Connection c ) if( time()-c->connected > max_time ) max_time = time()-c->connected; return format_time( max_time ); } string status() { string res = "<table>"; res += "<tr> <td colspan=2>"+ ""+stream->status()+"</td></tr>" "<tr><td>Connections:</td><td>"+connections+"/"+max_connections+" ("+ accessed+" since server start, "+denied+" denied)</td></tr><tr>" "<tr><td>Average listen time:</td><td>"+avg_listen_time()+"</td></tr>" "<tr><td>Longest listen time:</td><td>"+longest_listen_time()+"</td></tr>" "<tr><td>Current connections:</td><td>"; foreach( conn, Connection c )
8626872001-04-17Per Hedbor  catch { res += " "+c->status()+"\n"; };
b779eb2001-04-10Per Hedbor  return res+"</td></tr></table>"; }
650fea2001-04-10Per Hedbor } mapping(string:Location) locations = ([]);
b779eb2001-04-10Per Hedbor int __id=1;
650fea2001-04-10Per Hedbor class Connection { Stdio.File fd; MPEGStream stream;
d3cea92001-05-16Per Hedbor  Location location; int do_meta, meta_cnt;
b779eb2001-04-10Per Hedbor  string protocol;
650fea2001-04-10Per Hedbor  int sent, skipped; // frames.
d3cea92001-05-16Per Hedbor  int sent_bytes, sent_meta;
b779eb2001-04-10Per Hedbor  int id = __id++;
650fea2001-04-10Per Hedbor  array buffer = ({}); string current_block; function _ccb;
b779eb2001-04-10Per Hedbor  mapping current_md; int connected = time(); string status() {
8626872001-04-17Per Hedbor  if(!fd) return "Closed stream\n";
b779eb2001-04-10Per Hedbor  return sprintf( "%d. %s Time: %ds Remote: %s " "%d sent, %d skipped<br />",id,protocol, time()-connected, (fd->query_address()/" ")[0],
6877ce2001-04-11Per Hedbor  sent, skipped );
b779eb2001-04-10Per Hedbor  }
d3cea92001-05-16Per Hedbor  string old_mdkey;
b779eb2001-04-10Per Hedbor  string gen_metadata( ) {
d3cea92001-05-16Per Hedbor  string s = " "; if( !current_md )
b779eb2001-04-10Per Hedbor  current_md = stream->playlist->metadata();
d3cea92001-05-16Per Hedbor  if( (current_md->name||current_md->path)+current_md->url != old_mdkey ) { old_mdkey = (current_md->name||current_md->path)+current_md->url; s = sprintf( " StreamTitle='%s';StreamUrl='%s';", current_md->name || current_md->path, current_md->url || location->url ); } while( strlen(s) & 15 ) s+= "\0"; s[ 0 ]=strlen(s)/16; werror("%O\n", s );
b779eb2001-04-10Per Hedbor  return s; }
650fea2001-04-10Per Hedbor  static void callback( string frame ) { buffer += ({ frame });
c0c9b92001-04-11Per Hedbor  if( sizeof( buffer ) > 100 )
650fea2001-04-10Per Hedbor  { skipped++; buffer = buffer[1..]; }
6877ce2001-04-11Per Hedbor  if( sizeof( buffer ) == 1 )
c0c9b92001-04-11Per Hedbor  { remove_call_out( send_more ); call_out( send_more, 0.01 ); }
650fea2001-04-10Per Hedbor  }
b779eb2001-04-10Per Hedbor  int headers_done;
650fea2001-04-10Per Hedbor  static void send_more() { if( !strlen(current_block) ) {
b779eb2001-04-10Per Hedbor  headers_done = 1;
650fea2001-04-10Per Hedbor  if( !sizeof(buffer) ) return; current_block = buffer[0];
b779eb2001-04-10Per Hedbor  if( do_meta ) {
d3cea92001-05-16Per Hedbor  meta_cnt += strlen( current_block ); if( meta_cnt >= METAINTERVAL )
b779eb2001-04-10Per Hedbor  {
d3cea92001-05-16Per Hedbor  string meta; meta_cnt -= METAINTERVAL; int rest = strlen(current_block)-meta_cnt-1; sent_meta += strlen(meta = gen_metadata()); current_block = current_block[..rest]+meta+current_block[rest];
b779eb2001-04-10Per Hedbor  } }
650fea2001-04-10Per Hedbor  buffer = buffer[1..]; sent++; } int n = fd->write( current_block );
6877ce2001-04-11Per Hedbor  if( n > 0 ) { if( headers_done ) sent_bytes += n; current_block = current_block[n..]; }
8626872001-04-17Per Hedbor  else if( n < 0 ) closed();
650fea2001-04-10Per Hedbor  }
b779eb2001-04-10Per Hedbor  static void md_callback( mapping metadata ) { current_md = metadata; }
650fea2001-04-10Per Hedbor  static void closed( ) {
6877ce2001-04-11Per Hedbor  fd->set_blocking( );
650fea2001-04-10Per Hedbor  fd = 0; stream->remove_callback( callback );
b779eb2001-04-10Per Hedbor  stream->playlist->remove_md_callback( md_callback );
650fea2001-04-10Per Hedbor  _ccb( this_object() );
6877ce2001-04-11Per Hedbor  werror("Closed from client side\n"); // destruct( this_object() );
650fea2001-04-10Per Hedbor  }
b779eb2001-04-10Per Hedbor  static void create( Stdio.File _fd, string buffer, string prot, int _meta, MPEGStream _stream,
d3cea92001-05-16Per Hedbor  Location _loc, function _closed )
650fea2001-04-10Per Hedbor  {
d3cea92001-05-16Per Hedbor  location = _loc;
b779eb2001-04-10Per Hedbor  protocol = prot;
650fea2001-04-10Per Hedbor  fd = _fd;
b779eb2001-04-10Per Hedbor  do_meta = _meta;
650fea2001-04-10Per Hedbor  stream = _stream; _ccb = _closed; current_block = buffer;
8626872001-04-17Per Hedbor  fd->set_nonblocking( lambda(){}, send_more, closed );
b779eb2001-04-10Per Hedbor  if( stream ) stream->add_callback( callback ); if( stream->playlist ) stream->playlist->add_md_callback( md_callback );
650fea2001-04-10Per Hedbor  } } class VarStreams { inherit Variable.List; constant type="StreamList";
c0c9b92001-04-11Per Hedbor  // loc
650fea2001-04-10Per Hedbor  // playlist // jingle // maxconn
c0c9b92001-04-11Per Hedbor  // URL // name
650fea2001-04-10Per Hedbor  string render_row( string prefix, mixed val, int width ) { string res = "<input type=hidden name='"+prefix+"' value='"+prefix+"' />";
c0c9b92001-04-11Per Hedbor  mapping m = decode_stream( val ); res += "<table>"; res += "<tr><td>Location "+query_location()+"</td><td>"; res += ("<input name='"+prefix+"loc' value='"+ Roxen.html_encode_string(m->location||"")+"' size=20/>"); res += "</td><td>Source</td><td>";
650fea2001-04-10Per Hedbor  res += "<select name='"+prefix+"playlist'>"; mapping pl; foreach( sort(indices(pl=playlists(0))), string p )
c0c9b92001-04-11Per Hedbor  if( pl[ p ]->sname() == m->playlist ) res +="<option value='"+pl[p]->sname()+"' selected='t'>"+p+"</option>";
650fea2001-04-10Per Hedbor  else
c0c9b92001-04-11Per Hedbor  res +="<option value='"+pl[p]->sname()+"'>"+p+"</option>"; res += "</select></td></tr><tr>"; res += ("<td>Max. conn</td><td><input name='"+prefix+"conn' value='"+ (m->maxconn||10)+"' size=5/></td> "); res += ("<td>Name</td><td> <input name='"+prefix+"name' value='"+ Roxen.html_encode_string(m->name || "")+"' size=20/></td></tr>"); res += ("<tr><td>Jingle</td><td colspan=3>" "<input name='"+prefix+"jingle' value='"+ Roxen.html_encode_string(m->jingle || "")+"' size=50/></td></tr>"); res += ("<td>URL</td><td colspan=3> <input name='"+prefix+"url' value='"+ Roxen.html_encode_string(m->url || "")+"' size=50/></td>"); return res+"</tr></table>";
650fea2001-04-10Per Hedbor  } string transform_from_form( string v, mapping va ) { if( v == "" ) return "\0\0\0\0"; v = v[strlen(path())..]; return va[v+"loc"]+"\0"+va[v+"playlist"]+"\0"+va[v+"jingle"]
c0c9b92001-04-11Per Hedbor  +"\0"+va[v+"conn"]+"\0"+va[v+"url"]+"\0"+va[v+"name"];
650fea2001-04-10Per Hedbor  }
c0c9b92001-04-11Per Hedbor  mapping decode_stream( string s ) { mapping m = ([]); array a = s/"\0"; m->location = a[0]; if( sizeof( a ) > 1 ) m->playlist = a[1]; if( sizeof( a ) > 2 ) m->jingle = a[2]; if( sizeof( a ) > 3 ) m->maxconn = (int)a[3]; if( sizeof( a ) > 4 ) m->url = a[4]; if( sizeof( a ) > 5 ) m->name = a[5];
d3cea92001-05-16Per Hedbor  if( !m->jingle || !strlen( m->jingle ) ) m_delete( m, "jingle" ); if( !m->url || !strlen( m->url ) ) m_delete( m, "url" ); if( !m->name || !strlen( m->name ) ) m_delete( m, "name" );
c0c9b92001-04-11Per Hedbor  return m; }
650fea2001-04-10Per Hedbor  array(mapping) get_streams() {
c0c9b92001-04-11Per Hedbor  return map( query(), decode_stream );
650fea2001-04-10Per Hedbor  } } void create() { defvar( "streams", VarStreams( ({}), 0, _(0,"Streams"), _(0,"All the streams") ) ); defvar("location", "/strm/", _(0,"Mount point"), TYPE_LOCATION|VAR_INITIAL, _(0,"Where the module will be mounted in the site's virtual " "file system.")); } mapping playlists(int s) { mapping res = ([ ]); foreach( my_configuration()->get_providers( "icecast:playlist" ), Playlist m ) { if( s ) res[ m->sname() ] = m; else res[ m->pl_name() ] = m; } return res; } mapping streams = ([ ]);
b779eb2001-04-10Per Hedbor void st2()
650fea2001-04-10Per Hedbor { mapping pl = playlists( 1 ); foreach( getvar( "streams" )->get_streams(), mapping strm ) { MPEGStream mps;
b779eb2001-04-10Per Hedbor  if( strm->playlist && pl[ strm->playlist ] )
650fea2001-04-10Per Hedbor  if( !(mps = streams[ pl[strm->playlist] ]) ) { mps = streams[pl[strm->playlist]] = MPEGStream( pl[strm->playlist] ); mps->start(); }
b779eb2001-04-10Per Hedbor  if( !mps ) continue;
650fea2001-04-10Per Hedbor  if( !locations[ strm->location ] ) locations[ strm->location ] = Location( strm->location, (strm->jingle? Stdio.read_file( strm->jingle ) : 0 ), mps,
c0c9b92001-04-11Per Hedbor  (int)strm->maxconn, strm->url, strm->name );
650fea2001-04-10Per Hedbor  } }
b779eb2001-04-10Per Hedbor void start() { call_out( st2, 0.5 ); }
650fea2001-04-10Per Hedbor mapping find_file( string f, RequestID id ) { if( locations[f] ) return locations[f]->handle( id ); return 0; }
b779eb2001-04-10Per Hedbor  string status() { string res = "<table>"; foreach( indices( locations ), string loc ) { res += "<tr><td valign=top>"+loc+"</td><td valign=top>"+ locations[loc]->status()+"</td></tr>"; } return res+"</table>"; }