diff --git a/bin/channel_run.inc b/bin/channel_run.inc index 8c18312b..ca29f1be 100644 --- a/bin/channel_run.inc +++ b/bin/channel_run.inc @@ -23,6 +23,8 @@ define('NO_SESSION','1'); define('CLI', 1); +$chunk_buffer = ''; +$nb_chunks_remainder = 0; $path = dirname(__FILE__); $prefix = realpath($path . '/../'); require_once $prefix . '/lib/init.php'; @@ -42,11 +44,11 @@ if ($cargv > 1) { if ($_SERVER['argv'][$x] == "-c" && ($x + 1) < $cargv) { $chanid = intval($_SERVER['argv'][++$x]); $operations_string .= "\n\t" . T_('- Channel ' . $chanid); - } + } elseif ($_SERVER['argv'][$x] == "-v") { $operations_string .= "\n\t" . T_('- Verbose'); $verbose = true; - } + } elseif ($_SERVER['argv'][$x] == "-p" && ($x + 1) < $cargv) { $port = intval($_SERVER['argv'][++$x]); $operations_string .= "\n\t" . T_('- Port ' . $port); @@ -56,7 +58,7 @@ if ($cargv > 1) { if ($chanid <= 0) { usage(); - exit; + exit; } // Transcode is mandatory to have consistent stream codec @@ -66,7 +68,7 @@ if ($transcode_cfg == 'never') { die('Cannot start channel, transcoding is mandatory to work.'); } -echo T_("Starting Channel...") . $operations_string . "\n"; +echo T_("Starting Channel...") . $operations_string . "\n"; $channel = new Channel($chanid); if (!$channel->id) { @@ -106,7 +108,7 @@ echo T_("Listening on ") . $address . ':' . $port . "\n"; $stream_clients = array(); $client_socks = array(); -$last_stream = 0; +$last_stream = microtime(true); while(true) { //prepare readable sockets @@ -114,7 +116,6 @@ while(true) if (count($client_socks) < $channel->max_listeners) { $read_socks[] = $server; } - //echo "b\n";ob_flush(); //start reading and use a large timeout if(stream_select ( $read_socks, $write, $except, 1)) @@ -123,7 +124,7 @@ while(true) if (in_array($server, $read_socks)) { $new_client = stream_socket_accept($server); - + if ($new_client) { debug_event('channel', 'Connection accepted from ' . stream_socket_get_name($new_client, true) . '.', '5'); @@ -133,287 +134,120 @@ while(true) echo "New client connected.\n"; ob_flush(); } - + //delete the server socket from the read sockets unset($read_socks[array_search($server, $read_socks)]); } - + // Get new message from existing client foreach($read_socks as $sock) { - $data = fread($sock, 1024); - if(!$data) - { - client_disconnect($channel, $client_socks, $stream_clients, $sock); - continue; - } - - $headers = explode("\n", $data); - - if (count($headers) > 0) { - $cmd = explode(" ", $headers[0]); - if ($cmd['0'] == 'GET') { - switch ($cmd['1']) { - case '/stream.' . $channel->stream_type: - $options = array( - 'socket' => $sock, - 'length' => 0 - ); - - for ($i = 1; $i < count($headers); $i++) { - $headerpart = explode(":", $headers[$i], 2); - $header = strtolower(trim($headerpart[0])); - $value = trim($headerpart[1]); - switch ($header) { - case 'icy-metadata': - $options['metadata'] = ($value == '1'); - $options['metadata_lastsent'] = 0; - $options['metadata_lastsong'] = 0; - break; - } - } - - // Stream request - if ($options['metadata']) { - //fwrite($sock, "ICY 200 OK\r\n"); - fwrite($sock, "HTTP/1.0 200 OK\r\n"); - } else { - fwrite($sock, "HTTP/1.1 200 OK\r\n"); - fwrite($sock, "Cache-Control: no-store, no-cache, must-revalidate\r\n"); - } - fwrite($sock, "Content-Type: " . Song::type_to_mime($channel->stream_type) . "\r\n"); - fwrite($sock, "Accept-Ranges: none\r\n"); - - $genre = $channel->get_genre(); - // Send Shoutcast metadata on demand - if ($options['metadata']) { - fwrite($sock, "icy-notice1: " . AmpConfig::get('site_title') . "\r\n"); - fwrite($sock, "icy-name: " . $channel->name . "\r\n"); - if (!empty($genre)) { - fwrite($sock, "icy-genre: " . $genre . "\r\n"); - } - fwrite($sock, "icy-url: " . $channel->url . "\r\n"); - fwrite($sock, "icy-pub: " . ($channel->is_private) ? '0' : '1' . "\r\n"); - if ($channel->bitrate) { - fwrite($sock, "icy-br: " . strval($channel->bitrate) . "\r\n"); - } - fwrite($sock, "icy-metaint: " . strval($metadata_interval) . "\r\n"); - } - // Send additional Icecast metadata - fwrite($sock, "x-audiocast-server-url: " . $channel->url . "\r\n"); - fwrite($sock, "x-audiocast-name: " . $channel->name . "\r\n"); - fwrite($sock, "x-audiocast-description: " . $channel->description . "\r\n"); - fwrite($sock, "x-audiocast-url: " . $channel->url . "\r\n"); - if (!empty($genre)) { - fwrite($sock, "x-audiocast-genre: " . $genre . "\r\n"); - } - fwrite($sock, "x-audiocast-bitrate: " . strval($channel->bitrate) . "\r\n"); - fwrite($sock, "x-audiocast-public: " . (($channel->is_private) ? "0" : "1") . "\r\n"); - - fwrite($sock, "\r\n"); - - // Add to stream clients list - $key = array_search($sock, $read_socks); - $stream_clients[$key] = $options; - break; - - case '/': - case '/status.xsl': - // Stream request - fwrite($sock, "HTTP/1.0 200 OK\r\n"); - fwrite($sock, "Cache-Control: no-store, no-cache, must-revalidate\r\n"); - fwrite($sock, "Content-Type: text/html\r\n"); - fwrite($sock, "\r\n"); - - // Create xsl structure - - // Header - $xsl = ""; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "Icecast Streaming Media Server - Ampache" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "
" . "\n"; - - // Content - $xsl .= "
" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "\"\"" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "

Mount Point /stream." . $channel->stream_type . "

" . "\n"; - $xsl .= "stream_type .".m3u\">M3U" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $genre = $channel->get_genre(); - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $currentsong = ""; - if ($channel->media) { - $currentsong = $channel->media->f_artist . " - " . $channel->media->f_title; - } - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "
Stream Title:" . $channel->name . "
Stream Description:" . $channel->description . "
Content Type:" . Song::type_to_mime($channel->stream_type) . "
Mount Start:" . date("c", $channel->start_date) . "
Bitrate:" . $channel->bitrate . "
Current Listeners:" . $channel->listeners . "
Peak Listeners:" . $channel->peak_listeners . "
Stream Genre:" . $genre . "
Stream URL:url . "\" target=\"_blank\">" . $channel->url . "
Current Song:" . $currentsong . "
" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "\"\"" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "

" . "\n"; - - // Footer - $xsl .= "
" . "\n"; - $xsl .= "Support Icecast development at www.icecast.org" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "
" . "\n"; - $xsl .= "" . "\n"; - $xsl .= "" . "\n"; - - fwrite($sock, $xsl); - - fclose($sock); - unset($client_socks[array_search($sock, $client_socks)]); - break; - - case '/style.css': - case '/favicon.ico': - case '/images/corner_bottomleft.jpg': - case '/images/corner_bottomright.jpg': - case '/images/corner_topleft.jpg': - case '/images/corner_topright.jpg': - case '/images/icecast.png': - case '/images/key.png': - case '/images/tunein.png': - // Get read file data - $fpath = AmpConfig::get('prefix') . '/channel' . $cmd['1']; - $pinfo = pathinfo($fpath); - - $content_type = 'text/html'; - switch ($pinfo['extension']) { - case 'css': - $content_type = "text/css"; - break; - case 'jpg': - $content_type = "image/jpeg"; - break; - case 'png': - $content_type = "image/png"; - break; - case 'ico': - $content_type = "image/vnd.microsoft.icon"; - break; - } - fwrite($sock, "HTTP/1.0 200 OK\r\n"); - fwrite($sock, "Content-Type: " . $content_type . "\r\n"); - $fdata = file_get_contents($fpath); - fwrite($sock, "Content-Length: " . strlen($fdata) . "\r\n"); - fwrite($sock, "\r\n"); - fwrite($sock, $fdata); - fclose($sock); - unset($client_socks[array_search($sock, $client_socks)]); - break; - case '/stream.' . $channel->stream_type . '.m3u': - fwrite($sock, "HTTP/1.0 200 OK\r\n"); - fwrite($sock, "Cache-control: public\r\n"); - fwrite($sock, "Content-Disposition: filename=stream." . $channel->stream_type . ".m3u\r\n"); - fwrite($sock, "Content-Type: audio/x-mpegurl\r\n"); - fwrite($sock, "\r\n"); - - fwrite($sock, $channel->get_stream_url() . "\n"); - - fclose($sock); - unset($client_socks[array_search($sock, $client_socks)]); - break; - default: - debug_event('channel', 'Unknown request. Closing connection.', '3'); - fclose($sock); - unset($client_socks[array_search($sock, $client_socks)]); - break; - } - } - } // Handle data parse + http_serve($channel, $client_socks, $stream_clients, $read_socks, $sock); } } - + if ($channel->bitrate) { + $time_offset = microtime(true) - $last_stream; - $mtime = ($last_stream > 0 && $time_offset < 1) ? $time_offset : 1; - if ($last_stream > 0 && $time_offset < 1) { - usleep(1000000 - ($time_offset * 1000000)); - } elseif ($last_stream > 0) { - //$mtime = $time_offset; + + //debug_event('channel', 'time_offset : '. $time_offset, '5'); + //debug_event('channel', 'last_stream: '.$last_stream, '5'); + + if ($time_offset < 1) + usleep(1000000 - ($time_offset * 1000000)); // always at least 1 second between cycles + + $last_stream = microtime(true); + $mtime = ($time_offset > 1) ? $time_offset : 1; + $nb_chunks = ceil(($mtime * ($channel->bitrate+1/100*$channel->bitrate) * 1000 / 8) / $channel->chunk_size); // channel->bitrate+1% ... leave some headroom for metadata / headers + + // we only send full blocks, save remainder and apply when appropriate: allows more granular/arbitrary average bitrates + if ($nb_chunks - ($mtime * ($channel->bitrate+1/100*$channel->bitrate) * 1000 / 8 / $channel->chunk_size) > 0) + $nb_chunks_remainder += $nb_chunks - ($mtime * $channel->bitrate * 1000 / 8 / $channel->chunk_size); + if ($nb_chunks >= 1 && $nb_chunks_remainder >= 1){ + $nb_chunks -= 1; + $nb_chunks_remainder -= 1; + //debug_event('channel', 'REMAINDER: '.$nb_chunks_remainder, '5'); } - $nb_chunks = ceil(($mtime * $channel->bitrate * 1000) / 4096); + //debug_event('channel', 'mtime '.$mtime, '5'); + //debug_event('channel', 'nb_chunks: '.$nb_chunks, '5'); + } else { $nb_chunks = 1; } - + // Get multiple chunks according to bitrate to return enough data per second (because sleep with socket select) for ($c = 0; $c < $nb_chunks; $c++) { + $chunk = $channel->get_chunk(); $chunklen = strlen($chunk); + $chunk_buffer .= $chunk; + + //buffer maintenance + while (strlen($chunk_buffer) > (15 * $nb_chunks * $channel->chunk_size) ){ // buffer 15 seconds + + if (strtolower($channel->stream_type) == "ogg" && strtohex(substr($chunk_buffer, 0, 4)) == "4F676753") { //maintain ogg chunk alignment --- "4F676753" == "OggS" + // read OggS segment length + $hex = strtohex(substr($chunk_buffer, 0, 27)); + $ogg_nr_of_segments = hexdec(substr($hex, 26*2, 2)); + $hex .= strtohex(substr($chunk_buffer, 27, $ogg_nr_of_segments)); + $ogg_sum_segm_laces = 0; + for($segm = 0; $segm < $ogg_nr_of_segments; $segm++){ + $ogg_sum_segm_laces += hexdec(substr($hex, 27*2 + $segm*2, 2)); + } + //$naive = strpos(substr($chunk_buffer, 4), 'OggS') + 4; // naive search for next header + //remove 1 whole OggS chunk + $chunk_buffer = substr($chunk_buffer, 27 + $ogg_nr_of_segments + $ogg_sum_segm_laces); + //debug_event('channel', '$new chunk buffer : '.substr($chunk_buffer,0,300) . ' $hex: '.strtohex(substr($chunk_buffer,0,600)) . ' $ogg_nr_of_segments: ' .$ogg_nr_of_segments . ' bytes cut off: '.(27 + $ogg_nr_of_segments + $ogg_sum_segm_laces) . ' naive: ' .$naive, '5'); + } elseif (strtolower($channel->stream_type) == "ogg") { + debug_event('channel', 'Ogg alignament broken! Trying repair...', '5'); + $manual_search = strpos($chunk_buffer, 'OggS'); + $chunk_buffer = substr($chunk_buffer, $manual_search); + } else { // no chunk alignment required + $chunk_buffer = substr($chunk_buffer, $chunklen); + } + //debug_event('channel', 'remvd chunk from buffer ', '5'); + } + if ($chunklen > 0) { foreach($stream_clients as $key => $client) { $sock = $client['socket']; + $clchunk = $chunk; + if(!is_resource($sock)) { client_disconnect($channel, $client_socks, $stream_clients, $sock); continue; } - - $clchunk = $chunk; + + if ($client['isnew'] == 1){ + $client['isnew'] = 0; + //fwrite($sock, $channel->header_chunk); + //debug_event('channel', 'IS NEW' . $channel->header_chunk, '5'); + $clchunk_buffer = $channel->header_chunk . $chunk_buffer; + if ($client['metadata']){ //stub + //if (strtolower($channel->stream_type) == "ogg") + while(strlen($clchunk_buffer) > $metadata_interval){ + fwrite($sock, substr($clchunk_buffer, 0, $metadata_interval) . chr(0x00)); + $clchunk_buffer = substr($clchunk_buffer, $metadata_interval); + } + fwrite($sock, $clchunk_buffer); + $client['metadata_lastsent'] = 0; + $client['length'] += strlen($clchunk_buffer); + } else { + //fwrite($sock, $channel->header_chunk); + $buffer_bytes_written = fwrite($sock, $clchunk_buffer); + while ($buffer_bytes_written != strlen($clchunk_buffer)){ + debug_event('channel', 'I HERPED WHEN I SHOULD HAVE DERPED!', '5'); + //debug_event('channel', 'chunk_buffer bytes written:' .$buffer_bytes_written .'strlen $chunk_buffer: '.strlen($chunk_buffer), '5'); + $clchunk_buffer = substr($clchunk_buffer, $buffer_bytes_written); + $buffer_bytes_written = fwrite($sock, $clchunk_buffer); + } + } + $stream_clients[$key] = $client; + continue; + } + // Check if we need to insert metadata information if ($client['metadata']) { $chkmdlen = ($client['length'] + $chunklen) - $client['metadata_lastsent']; @@ -436,11 +270,11 @@ while(true) $clchunk = substr($chunk, $subpos); } } + if (strlen($clchunk) > 0) { fwrite($sock, $clchunk); $client['length'] += strlen($clchunk); } - $stream_clients[$key] = $client; //debug_event('channel', 'Client stream current length: ' . $client['length'], '5'); } @@ -448,12 +282,10 @@ while(true) $channel->update_listeners(0); die('No more data, stream ended.'); } - - $last_stream = microtime(true); } } -ob_end_flush(); +ob_end_flush(); echo "\n"; function client_disconnect($channel, &$client_socks, &$stream_clients, $sock) @@ -473,7 +305,7 @@ function usage() echo T_("- Channel Listening -"); echo "\n"; echo T_("Usage: channel_run.inc [-c {CHANNEL ID}|-p {PORT}|-v]"); - echo "\n\t"; + echo "\n\t"; echo "\n-c {CHANNEL ID}\t"; echo T_('Channel id to start'); echo "\n-p {PORT}\t"; @@ -485,4 +317,261 @@ function usage() echo "\n"; } +function http_serve($channel, &$client_socks, &$stream_clients, &$read_socks, $sock) +{ + $data = fread($sock, 1024); + if(!$data) + { + client_disconnect($channel, $client_socks, $stream_clients, $sock); + return; + } + + $headers = explode("\n", $data); + + if (count($headers) > 0) { + $cmd = explode(" ", $headers[0]); + if ($cmd['0'] == 'GET') { + switch ($cmd['1']) { + case '/stream.' . $channel->stream_type: + $options = array( + 'socket' => $sock, + 'length' => 0, + 'isnew' => 1 + ); + + //debug_event('channel', 'HTTP HEADERS: '.$data,'5'); + for ($i = 1; $i < count($headers); $i++) { + $headerpart = explode(":", $headers[$i], 2); + $header = strtolower(trim($headerpart[0])); + $value = trim($headerpart[1]); + switch ($header) { + case 'icy-metadata': + $options['metadata'] = ($value == '1'); + $options['metadata_lastsent'] = 0; + $options['metadata_lastsong'] = 0; + break; + } + } + + // Stream request + if ($options['metadata']) { + //$http = "ICY 200 OK\r\n"); + $http = "HTTP/1.0 200 OK\r\n"; + } else { + $http = "HTTP/1.1 200 OK\r\n"; + $http .= "Cache-Control: no-store, no-cache, must-revalidate\r\n"; + } + $http .= "Content-Type: " . Song::type_to_mime($channel->stream_type) . "\r\n"; + $http .= "Accept-Ranges: none\r\n"; + + $genre = $channel->get_genre(); + // Send Shoutcast metadata on demand + //if ($options['metadata']) { + $http .= "icy-notice1: " . AmpConfig::get('site_title') . "\r\n"; + $http .= "icy-name: " . $channel->name . "\r\n"; + if (!empty($genre)) { + $http .= "icy-genre: " . $genre . "\r\n"; + } + $http .= "icy-url: " . $channel->url . "\r\n"; + $http .= "icy-pub: " . (($channel->is_private) ? "0" : "1") . "\r\n"; + if ($channel->bitrate) { + $http .= "icy-br: " . strval($channel->bitrate) . "\r\n"; + } + global $metadata_interval; + $http .= "icy-metaint: " . strval($metadata_interval) . "\r\n"; + //} + // Send additional Icecast metadata + $http .= "x-audiocast-server-url: " . $channel->url . "\r\n"; + $http .= "x-audiocast-name: " . $channel->name . "\r\n"; + $http .= "x-audiocast-description: " . $channel->description . "\r\n"; + $http .= "x-audiocast-url: " . $channel->url . "\r\n"; + if (!empty($genre)) { + $http .= "x-audiocast-genre: " . $genre . "\r\n"; + } + $http .= "x-audiocast-bitrate: " . strval($channel->bitrate) . "\r\n"; + $http .= "x-audiocast-public: " . (($channel->is_private) ? "0" : "1") . "\r\n"; + + $http .= "\r\n"; + + fwrite($sock, $http); + + // Add to stream clients list + $key = array_search($sock, $read_socks); + $stream_clients[$key] = $options; + break; + + case '/': + case '/status.xsl': + // Stream request + fwrite($sock, "HTTP/1.0 200 OK\r\n"); + fwrite($sock, "Cache-Control: no-store, no-cache, must-revalidate\r\n"); + fwrite($sock, "Content-Type: text/html\r\n"); + fwrite($sock, "\r\n"); + + // Create xsl structure + + // Header + $xsl = ""; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "Icecast Streaming Media Server - Ampache" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "
" . "\n"; + + // Content + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "\"\"" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "

Mount Point /stream." . $channel->stream_type . "

" . "\n"; + $xsl .= "stream_type .".m3u\">M3U" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $genre = $channel->get_genre(); + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $currentsong = ""; + if ($channel->media) { + $currentsong = $channel->media->f_artist . " - " . $channel->media->f_title; + } + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "
Stream Title:" . $channel->name . "
Stream Description:" . $channel->description . "
Content Type:" . Song::type_to_mime($channel->stream_type) . "
Mount Start:" . date("c", $channel->start_date) . "
Bitrate:" . $channel->bitrate . "
Current Listeners:" . $channel->listeners . "
Peak Listeners:" . $channel->peak_listeners . "
Stream Genre:" . $genre . "
Stream URL:url . "\" target=\"_blank\">" . $channel->url . "
Current Song:" . $currentsong . "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "\"\"" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "

" . "\n"; + + // Footer + $xsl .= "
" . "\n"; + $xsl .= "Support Icecast development at www.icecast.org" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + + fwrite($sock, $xsl); + + fclose($sock); + unset($client_socks[array_search($sock, $client_socks)]); + break; + + case '/style.css': + case '/favicon.ico': + case '/images/corner_bottomleft.jpg': + case '/images/corner_bottomright.jpg': + case '/images/corner_topleft.jpg': + case '/images/corner_topright.jpg': + case '/images/icecast.png': + case '/images/key.png': + case '/images/tunein.png': + // Get read file data + $fpath = AmpConfig::get('prefix') . '/channel' . $cmd['1']; + $pinfo = pathinfo($fpath); + + $content_type = 'text/html'; + switch ($pinfo['extension']) { + case 'css': + $content_type = "text/css"; + break; + case 'jpg': + $content_type = "image/jpeg"; + break; + case 'png': + $content_type = "image/png"; + break; + case 'ico': + $content_type = "image/vnd.microsoft.icon"; + break; + } + fwrite($sock, "HTTP/1.0 200 OK\r\n"); + fwrite($sock, "Content-Type: " . $content_type . "\r\n"); + $fdata = file_get_contents($fpath); + fwrite($sock, "Content-Length: " . strlen($fdata) . "\r\n"); + fwrite($sock, "\r\n"); + fwrite($sock, $fdata); + fclose($sock); + unset($client_socks[array_search($sock, $client_socks)]); + break; + case '/stream.' . $channel->stream_type . '.m3u': + fwrite($sock, "HTTP/1.0 200 OK\r\n"); + fwrite($sock, "Cache-control: public\r\n"); + fwrite($sock, "Content-Disposition: filename=stream." . $channel->stream_type . ".m3u\r\n"); + fwrite($sock, "Content-Type: audio/x-mpegurl\r\n"); + fwrite($sock, "\r\n"); + + fwrite($sock, $channel->get_stream_url() . "\n"); + + fclose($sock); + unset($client_socks[array_search($sock, $client_socks)]); + break; + default: + debug_event('channel', 'Unknown request. Closing connection.', '3'); + fclose($sock); + unset($client_socks[array_search($sock, $client_socks)]); + break; + } + } + } +} + +function strtohex($x) { + $s=''; + foreach(str_split($x) as $c) $s.=sprintf("%02X",ord($c)); + return($s); +} + ?> diff --git a/lib/class/channel.class.php b/lib/class/channel.class.php index 3bbae312..bdc03a49 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -39,6 +39,10 @@ class Channel extends database_object implements media, library_item public $name; public $description; + public $header_chunk; + public $chunk_size = 4096; + private $header_chunk_remainder = 0; + public $tags; public $f_tags; @@ -162,10 +166,10 @@ class Channel extends database_object implements media, library_item switch ($type) { case 'playlist': $ftype = $type; - break; + break; default: $ftype = ''; - break; + break; } return $ftype; @@ -221,9 +225,9 @@ class Channel extends database_object implements media, library_item $medias = array(); if (!$filter_type || $filter_type == 'channel') { $medias[] = array( - 'object_type' => 'channel', - 'object_id' => $this->id - ); + 'object_type' => 'channel', + 'object_id' => $this->id + ); } return $medias; } @@ -412,17 +416,54 @@ class Channel extends database_object implements media, library_item // Stream not yet initialized for this media, start it if (!$this->transcoder) { $options = array( - 'bitrate' => $this->bitrate - ); + 'bitrate' => $this->bitrate + ); $this->transcoder = Stream::start_transcode($this->media, $this->stream_type, null, $options); $this->media_bytes_streamed = 0; } if (is_resource($this->transcoder['handle'])) { - - $chunk = fread($this->transcoder['handle'], 4096); + if (ftell($this->transcoder['handle']) == 0) + $this->header_chunk = ''; + $chunk = fread($this->transcoder['handle'], $this->chunk_size); $this->media_bytes_streamed += strlen($chunk); + if ((ftell($this->transcoder['handle']) < 10000 && strtolower($this->stream_type) == "ogg") || $this->header_chunk_remainder) { + //debug_event('channel', 'File handle pointer: ' . ftell($this->transcoder['handle']) ,'5'); + $clchunk = $chunk; + + if ($this->header_chunk_remainder) { + $this->header_chunk .= substr($clchunk, 0, $this->header_chunk_remainder); + if (strlen($clchunk) >= $this->header_chunk_remainder) { + $clchunk = substr($clchunk, $this->header_chunk_remainder); + $this->header_chunk_remainder = 0; + } else { + $this->header_chunk_remainder = $this->header_chunk_remainder - strlen($clchunk); + $clchunk = ''; + } + } + // see bin/channel_run.inc for explanation what's happening here + while ($this->strtohex(substr($clchunk, 0, 4)) == "4F676753") { + $hex = $this->strtohex(substr($clchunk, 0, 27)); + $ogg_nr_of_segments = hexdec(substr($hex, 26*2, 2)); + if ((substr($clchunk, 27 + $ogg_nr_of_segments + 1, 6) == "vorbis") || (substr($clchunk, 27 + $ogg_nr_of_segments, 4) == "Opus")) { + $hex .= $this->strtohex(substr($clchunk, 27, $ogg_nr_of_segments)); + $ogg_sum_segm_laces = 0; + for ($segm = 0; $segm < $ogg_nr_of_segments; $segm++) { + $ogg_sum_segm_laces += hexdec(substr($hex, 27*2 + $segm*2, 2)); + } + $this->header_chunk .= substr($clchunk, 0, 27 + $ogg_nr_of_segments + $ogg_sum_segm_laces); + if (strlen($clchunk) < (27 + $ogg_nr_of_segments + $ogg_sum_segm_laces)) + $this->header_chunk_remainder = (int) (27 + $ogg_nr_of_segments + $ogg_sum_segm_laces - strlen($clchunk)); + $clchunk = substr($clchunk, 27 + $ogg_nr_of_segments + $ogg_sum_segm_laces); + } else //no more interesting headers + $clchunk = ''; + } + } + //debug_event('channel', 'File handle pointer: ' . ftell($this->transcoder['handle']) ,'5'); + //debug_event('channel', 'CHUNK : ' . $chunk, '5'); + //debug_event('channel', 'Chunk size: ' . strlen($chunk) ,'5'); + // End of file, prepare to move on for next call if (feof($this->transcoder['handle'])) { $this->media->set_played(-1, 'Ampache', array()); @@ -493,4 +534,12 @@ class Channel extends database_object implements media, library_item } + private function strtohex($x) + { + $s=''; + foreach(str_split($x) as $c) $s.=sprintf("%02X",ord($c)); + return($s); + } + + } // end of channel class