From 512f6aeeb58dacfe466b1728a41014d1424465bd Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Thu, 28 May 2015 20:50:06 +0200 Subject: [PATCH 1/9] Enables Ogg (also works with Opus) channel streaming - bin/channel_run.inc: * extensive rewrite and restructuring * implements a variable-size server-side buffer * small bugfixes here and there * allow for more precise bitrate handling (via $nb_chunks_remainder) * small improvements to http serving - lib/class/channel.class.php: * add $header_chunk variable and remember header of current filer for later use in channel streaming * added $chunk_size variable --- bin/channel_run.inc | 609 +++++++++++++++++++++--------------- lib/class/channel.class.php | 17 +- 2 files changed, 364 insertions(+), 262 deletions(-) diff --git a/bin/channel_run.inc b/bin/channel_run.inc index 8c18312b..4f135558 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'; @@ -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,8 +282,6 @@ while(true) $channel->update_listeners(0); die('No more data, stream ended.'); } - - $last_stream = microtime(true); } } @@ -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..b6b428df 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -39,6 +39,9 @@ class Channel extends database_object implements media, library_item public $name; public $description; + public $header_chunk; + public $chunk_size = 4096; + public $tags; public $f_tags; @@ -419,8 +422,18 @@ class Channel extends database_object implements media, library_item } if (is_resource($this->transcoder['handle'])) { - - $chunk = fread($this->transcoder['handle'], 4096); + if ( ftell($this->transcoder['handle']) == 0 ) { + debug_event('channel', 'File handle pointer: ' . ftell($this->transcoder['handle']) ,'3'); + $this->header_chunk = ''; + } + $chunk = fread($this->transcoder['handle'], $this->chunk_size); + if ( ftell($this->transcoder['handle']) < 2000 ){ + $this->header_chunk .= $chunk; + //debug_event('channel', 'CHUNK : ' . $this->header_chunk, '3'); + } + //debug_event('channel', 'File handle pointer: ' . ftell($this->transcoder['handle']) ,'3'); + //debug_event('channel', 'CHUNK : ' . $chunk, '3'); + //debug_event('channel', 'Chunk size: ' . strlen($chunk) ,'3'); $this->media_bytes_streamed += strlen($chunk); // End of file, prepare to move on for next call From 133a4f0ff3dd7d82c0d0c8c4ee00995227dedf73 Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Fri, 29 May 2015 02:36:05 +0200 Subject: [PATCH 2/9] Smarter header detection for lib/class/channel.class.php --- lib/class/channel.class.php | 59 +++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/class/channel.class.php b/lib/class/channel.class.php index b6b428df..89db3e3d 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -41,6 +41,7 @@ class Channel extends database_object implements media, library_item public $header_chunk; public $chunk_size = 4096; + private $header_chunk_remainder = 0; public $tags; public $f_tags; @@ -422,20 +423,47 @@ class Channel extends database_object implements media, library_item } if (is_resource($this->transcoder['handle'])) { - if ( ftell($this->transcoder['handle']) == 0 ) { - debug_event('channel', 'File handle pointer: ' . ftell($this->transcoder['handle']) ,'3'); - $this->header_chunk = ''; - } - $chunk = fread($this->transcoder['handle'], $this->chunk_size); - if ( ftell($this->transcoder['handle']) < 2000 ){ - $this->header_chunk .= $chunk; - //debug_event('channel', 'CHUNK : ' . $this->header_chunk, '3'); - } - //debug_event('channel', 'File handle pointer: ' . ftell($this->transcoder['handle']) ,'3'); - //debug_event('channel', 'CHUNK : ' . $chunk, '3'); - //debug_event('channel', 'Chunk size: ' . strlen($chunk) ,'3'); + + $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']) ,'3'); + $clchunk = $chunk; + if ($this->transcoder['handle'] == 0) + $this->header_chunk = ''; + else { + $this->header_chunk .= substr($clchunk, 0, $this->header_chunk_remainder); + if (strlen($clchunk) >= $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 (strtohex(substr($clchunk, 0, 4)) == "4F676753"){ + $hex = 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 .= 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($chunk, 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 = 27 + $ogg_nr_of_segments + $ogg_sum_segm_laces - strlen($chunk); + $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']) ,'3'); + //debug_event('channel', 'CHUNK : ' . $chunk, '3'); + //debug_event('channel', 'Chunk size: ' . strlen($chunk) ,'3'); + // End of file, prepare to move on for next call if (feof($this->transcoder['handle'])) { $this->media->set_played(-1, 'Ampache', array()); @@ -506,4 +534,11 @@ class Channel extends database_object implements media, library_item } + function strtohex($x) { + $s=''; + foreach(str_split($x) as $c) $s.=sprintf("%02X",ord($c)); + return($s); + } + + } // end of channel class From 05e24e58fe8ca371957e979da7b891624e231549 Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Fri, 29 May 2015 02:51:07 +0200 Subject: [PATCH 3/9] bugfix --- lib/class/channel.class.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/class/channel.class.php b/lib/class/channel.class.php index 89db3e3d..87433aef 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -423,16 +423,16 @@ class Channel extends database_object implements media, library_item } if (is_resource($this->transcoder['handle'])) { - + if ($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 ) { + 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']) ,'3'); $clchunk = $chunk; - if ($this->transcoder['handle'] == 0) - $this->header_chunk = ''; - else { + + if ($this->header_chunk_remainder) { $this->header_chunk .= substr($clchunk, 0, $this->header_chunk_remainder); if (strlen($clchunk) >= $header_chunk_remainder){ $clchunk = substr($clchunk, $this->header_chunk_remainder); @@ -441,7 +441,7 @@ class Channel extends database_object implements media, library_item $this->header_chunk_remainder = $this->header_chunk_remainder - strlen($clchunk); $clchunk = ''; } - } + } // see bin/channel_run.inc for explanation what's happening here while (strtohex(substr($clchunk, 0, 4)) == "4F676753"){ $hex = strtohex(substr($clchunk, 0, 27)); From 2eb030e4eebe52e9276f8583017798e2ab415593 Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Fri, 29 May 2015 03:13:50 +0200 Subject: [PATCH 4/9] bugfix --- lib/class/channel.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class/channel.class.php b/lib/class/channel.class.php index 87433aef..a820da4e 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -423,7 +423,7 @@ class Channel extends database_object implements media, library_item } if (is_resource($this->transcoder['handle'])) { - if ($this->transcoder['handle'] == 0) + if (ftell($this->transcoder['handle']) == 0) $this->header_chunk = ''; $chunk = fread($this->transcoder['handle'], $this->chunk_size); $this->media_bytes_streamed += strlen($chunk); From 680bb9ae0f17b6c4c2404f434bdd6c6fb867dcf8 Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Fri, 29 May 2015 03:45:01 +0200 Subject: [PATCH 5/9] bugfix --- lib/class/channel.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class/channel.class.php b/lib/class/channel.class.php index a820da4e..9b1ea280 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -452,9 +452,9 @@ class Channel extends database_object implements media, library_item 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($chunk, 0, 27 + $ogg_nr_of_segments + $ogg_sum_segm_laces); + $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 = 27 + $ogg_nr_of_segments + $ogg_sum_segm_laces - strlen($chunk); + $this->header_chunk_remainder = 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 = ''; From 7764c44026ec28e6be76a343a684fe5ef6b8f861 Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Fri, 29 May 2015 04:19:03 +0200 Subject: [PATCH 6/9] indents --- lib/class/channel.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class/channel.class.php b/lib/class/channel.class.php index 9b1ea280..184e5236 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -416,8 +416,8 @@ 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; } @@ -462,7 +462,7 @@ class Channel extends database_object implements media, library_item } //debug_event('channel', 'File handle pointer: ' . ftell($this->transcoder['handle']) ,'3'); //debug_event('channel', 'CHUNK : ' . $chunk, '3'); - //debug_event('channel', 'Chunk size: ' . strlen($chunk) ,'3'); + //debug_event('channel', 'Chunk size: ' . strlen($chunk) ,'3'); // End of file, prepare to move on for next call if (feof($this->transcoder['handle'])) { From 4a9089124dce6f155b4729d2e2a7874a51c89463 Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Fri, 29 May 2015 04:42:28 +0200 Subject: [PATCH 7/9] more bugfixes --- lib/class/channel.class.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/class/channel.class.php b/lib/class/channel.class.php index 184e5236..d056bb9f 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -166,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; @@ -225,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; } @@ -429,12 +429,12 @@ class Channel extends database_object implements media, library_item $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']) ,'3'); + //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) >= $header_chunk_remainder){ + if (strlen($clchunk) >= $this->header_chunk_remainder){ $clchunk = substr($clchunk, $this->header_chunk_remainder); $this->header_chunk_remainder = 0; } else { @@ -454,15 +454,15 @@ class Channel extends database_object implements media, library_item } $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 = 27 + $ogg_nr_of_segments + $ogg_sum_segm_laces - strlen($clchunk); + $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']) ,'3'); - //debug_event('channel', 'CHUNK : ' . $chunk, '3'); - //debug_event('channel', 'Chunk size: ' . strlen($chunk) ,'3'); + //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'])) { @@ -534,7 +534,7 @@ class Channel extends database_object implements media, library_item } - function strtohex($x) { + private function strtohex($x) { $s=''; foreach(str_split($x) as $c) $s.=sprintf("%02X",ord($c)); return($s); From bf0769c5f2edde7fd99090c24b392c3883830cb8 Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Fri, 29 May 2015 05:07:06 +0200 Subject: [PATCH 8/9] fixing one bug, introducing new ones --- lib/class/channel.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class/channel.class.php b/lib/class/channel.class.php index d056bb9f..c8b2763d 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -443,11 +443,11 @@ class Channel extends database_object implements media, library_item } } // see bin/channel_run.inc for explanation what's happening here - while (strtohex(substr($clchunk, 0, 4)) == "4F676753"){ - $hex = strtohex(substr($clchunk, 0, 27)); + 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 .= strtohex(substr($clchunk, 27, $ogg_nr_of_segments)); + $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)); From 3daba0446a448529a515ff9e478da3912fef2abe Mon Sep 17 00:00:00 2001 From: Deathcrow Date: Sat, 30 May 2015 19:16:30 +0200 Subject: [PATCH 9/9] respect Ampache coding standards --- bin/channel_run.inc | 16 ++++++++-------- lib/class/channel.class.php | 13 +++++++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/bin/channel_run.inc b/bin/channel_run.inc index 4f135558..ca29f1be 100644 --- a/bin/channel_run.inc +++ b/bin/channel_run.inc @@ -44,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); @@ -58,7 +58,7 @@ if ($cargv > 1) { if ($chanid <= 0) { usage(); - exit; + exit; } // Transcode is mandatory to have consistent stream codec @@ -68,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) { @@ -285,7 +285,7 @@ while(true) } } -ob_end_flush(); +ob_end_flush(); echo "\n"; function client_disconnect($channel, &$client_socks, &$stream_clients, $sock) @@ -305,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"; @@ -364,7 +364,7 @@ function http_serve($channel, &$client_socks, &$stream_clients, &$read_socks, $s $http .= "Content-Type: " . Song::type_to_mime($channel->stream_type) . "\r\n"; $http .= "Accept-Ranges: none\r\n"; - $genre = $channel->get_genre(); + $genre = $channel->get_genre(); // Send Shoutcast metadata on demand //if ($options['metadata']) { $http .= "icy-notice1: " . AmpConfig::get('site_title') . "\r\n"; @@ -572,6 +572,6 @@ 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 c8b2763d..bdc03a49 100644 --- a/lib/class/channel.class.php +++ b/lib/class/channel.class.php @@ -428,13 +428,13 @@ class Channel extends database_object implements media, library_item $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){ + 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){ + if (strlen($clchunk) >= $this->header_chunk_remainder) { $clchunk = substr($clchunk, $this->header_chunk_remainder); $this->header_chunk_remainder = 0; } else { @@ -443,13 +443,13 @@ class Channel extends database_object implements media, library_item } } // see bin/channel_run.inc for explanation what's happening here - while ($this->strtohex(substr($clchunk, 0, 4)) == "4F676753"){ + 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")){ + 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++){ + 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); @@ -534,7 +534,8 @@ class Channel extends database_object implements media, library_item } - private function strtohex($x) { + private function strtohex($x) + { $s=''; foreach(str_split($x) as $c) $s.=sprintf("%02X",ord($c)); return($s);