1) {
for ($x = 1; $x < $cargv; $x++) {
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);
}
}
}
if ($chanid <= 0) {
usage();
exit;
}
// Transcode is mandatory to have consistent stream codec
$transcode_cfg = AmpConfig::get('transcode');
if ($transcode_cfg == 'never') {
die('Cannot start channel, transcoding is mandatory to work.');
}
echo T_("Starting Channel...") . $operations_string . "\n";
$channel = new Channel($chanid);
if (!$channel->id) {
die (T_("Unknown channel."));
}
if ($port <= 0) {
if ($channel->fixed_endpoint) {
$address = $channel->interface;
$port = $channel->port;
} else {
$address = "127.0.0.1";
// Try to find an available port
for ($p = 8200; $p < 8300; ++$p) {
$connection = @fsockopen($address, $p);
if (is_resource($connection)) {
fclose($connection);
} else {
echo T_("Found available port ") . $p . "\n";
$port = $p;
break;
}
}
}
}
ob_start();
$server_uri = 'tcp://' . $address . ':' . $port;
$server = stream_socket_server($server_uri, $errno, $errorMessage);
if ($server === false)
{
die("Could not bind to socket: " . $errorMessage);
}
$channel->update_start($start_date, $address, $port, getmypid());
echo T_("Listening on ") . $address . ':' . $port . "\n";
$stream_clients = array();
$client_socks = array();
$last_stream = microtime(true);
while(true)
{
//prepare readable sockets
$read_socks = $client_socks;
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))
{
//new client
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');
$client_socks[] = $new_client;
$channel->update_listeners(count($client_socks), true);
debug_event('channel', 'Now there are total '. count($client_socks) . ' clients.', '5');
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)
{
// Handle data parse
http_serve($channel, $client_socks, $stream_clients, $read_socks, $sock);
}
}
if ($channel->bitrate) {
$time_offset = microtime(true) - $last_stream;
//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');
}
//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;
}
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'];
if ($chkmdlen >= $metadata_interval) {
$subpos = ($client['metadata_lastsent'] + $metadata_interval) - $client['length'];
fwrite($sock, substr($clchunk, 0, $subpos));
$client['length'] += $subpos;
if ($channel->media->id != $client['metadata_lastsong']) {
$metadata = "StreamTitle='" . str_replace('-', ' ', $channel->media->f_artist) . "-" . $channel->media->f_title . "';";
$metadata .= chr(0x00);
$metadatalen = ceil(strlen($metadata) / 16);
$metadata = str_pad($metadata, $metadatalen * 16, chr(0x00), STR_PAD_RIGHT);
//debug_event('channel', 'Sending metadata to client...', '5');
fwrite($sock, chr($metadatalen) . $metadata);
$client['metadata_lastsong'] = $channel->media->id;
} else {
fwrite($sock, chr(0x00));
}
$client['metadata_lastsent'] = $client['length'];
$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');
}
} else {
$channel->update_listeners(0);
die('No more data, stream ended.');
}
}
}
ob_end_flush();
echo "\n";
function client_disconnect($channel, &$client_socks, &$stream_clients, $sock)
{
$key = array_search($sock, $client_socks);
unset($client_socks[$key]);
unset($stream_clients[$key]);
@fclose($sock);
$channel->update_listeners(count($client_socks));
debug_event('channel', 'A client disconnected. Now there are total '. count($client_socks) . ' clients.', '5');
echo "Client disconnected.\n";
ob_flush();
}
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-c {CHANNEL ID}\t";
echo T_('Channel id to start');
echo "\n-p {PORT}\t";
echo T_('Listening port, default get an available port automatically');
echo "\n-v\t";
echo T_('Verbose');
echo "\n";
echo "----------------------------------------------------------";
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 .= "Stream Title: | " . "\n";
$xsl .= "" . $channel->name . " | " . "\n";
$xsl .= "
" . "\n";
$xsl .= "" . "\n";
$xsl .= "Stream Description: | " . "\n";
$xsl .= "" . $channel->description . " | " . "\n";
$xsl .= "
" . "\n";
$xsl .= "" . "\n";
$xsl .= "Content Type: | " . "\n";
$xsl .= "" . Song::type_to_mime($channel->stream_type) . " | " . "\n";
$xsl .= "
" . "\n";
$xsl .= "" . "\n";
$xsl .= "Mount Start: | " . "\n";
$xsl .= "" . date("c", $channel->start_date) . " | " . "\n";
$xsl .= "
" . "\n";
$xsl .= "" . "\n";
$xsl .= "Bitrate: | " . "\n";
$xsl .= "" . $channel->bitrate . " | " . "\n";
$xsl .= "
" . "\n";
$xsl .= "" . "\n";
$xsl .= "Current Listeners: | " . "\n";
$xsl .= "" . $channel->listeners . " | " . "\n";
$xsl .= "
" . "\n";
$xsl .= "" . "\n";
$xsl .= "Peak Listeners: | " . "\n";
$xsl .= "" . $channel->peak_listeners . " | " . "\n";
$xsl .= "
" . "\n";
$genre = $channel->get_genre();
$xsl .= "" . "\n";
$xsl .= "Stream Genre: | " . "\n";
$xsl .= "" . $genre . " | " . "\n";
$xsl .= "
" . "\n";
$xsl .= "" . "\n";
$xsl .= "Stream URL: | " . "\n";
$xsl .= "url . "\" target=\"_blank\">" . $channel->url . " | " . "\n";
$xsl .= "
" . "\n";
$currentsong = "";
if ($channel->media) {
$currentsong = $channel->media->f_artist . " - " . $channel->media->f_title;
}
$xsl .= "" . "\n";
$xsl .= "Current Song: | " . "\n";
$xsl .= "" . $currentsong . " | " . "\n";
$xsl .= "
" . "\n";
$xsl .= "
" . "\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);
}
?>