Updated: 8 October 2026 · Applies to: PHP 8.3.35 and 8.4.26 (PHP-FPM behind nginx 1.26.3) with Icecast 2.4.4 on Debian 13, tested; Centova Cast 3.3.4 from Centova documentation
A PHP proxy is a small script on your HTTPS website that reads the radio stream from its plain http:// address on the server side and passes the audio on to the listener. Your web player then uses an https:// link on your own domain, so browsers stop blocking it as mixed content. The tested script is below. Every listener keeps one PHP process busy for as long as they listen, so use it only for a small station when you cannot change the web server or the streaming server. On Centova Cast 3.2.14 or newer (the current release is 3.3.4), use Centova's own stream proxy instead.
The script
- Save the code below as
radio.phpon your HTTPS website. It needs PHP's curl extension (php -mlistscurlwhen it is installed). - Set
STREAM_URLto your stream's direct link, for example from your control panel's tune-in links. Never take the address from the request (such as?url=): that would turn your website into an open proxy for anyone. - Use
https://www.example.com/radio.phpas the stream address in your web player.
<?php
// radio.php: re-streams one radio stream through this website (and its HTTPS certificate).
// Set STREAM_URL to your stream's direct address. Never take it from the request.
const STREAM_URL = 'http://127.0.0.1:8000/stream';
set_time_limit(0); // a listener may stay connected for hours
ignore_user_abort(true); // we notice the listener leaving ourselves (below)
while (ob_get_level() > 0) {
ob_end_clean(); // pass audio on as it arrives
}
$started = false;
$ch = curl_init(STREAM_URL);
curl_setopt_array($ch, [
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FAILONERROR => true,
CURLOPT_USERAGENT => 'radio-php-proxy',
CURLOPT_WRITEFUNCTION => function ($ch, string $data) use (&$started): int {
if (!$started) {
header('Content-Type: ' . (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'audio/mpeg'));
header('Cache-Control: no-cache, no-store');
header('X-Accel-Buffering: no'); // stops nginx from buffering the audio
$started = true;
}
echo $data;
flush();
return connection_aborted() ? 0 : strlen($data); // 0 closes the upstream connection
},
]);
curl_exec($ch);
if (!$started) {
http_response_code(502);
header('Content-Type: text/plain');
echo "The stream is offline.\n";
}
The script copies the stream's own content type, so AAC streams work as well as MP3.
What we tested
We ran Icecast 2.4.4 with a 64 kbps MP3 source and served the script with PHP-FPM 8.4 behind nginx, and with PHP 8.3's built-in server, all on 127.0.0.1 inside a container. Through the proxy the audio arrived at once:
$ curl -sS -D - -o /dev/null --max-time 3 http://127.0.0.1:8081/radio.php HTTP/1.1 200 OK Server: nginx ... Content-Type: audio/mpeg Transfer-Encoding: chunked Connection: keep-alive Cache-Control: no-cache, no-store curl: (28) Operation timed out after 3001 milliseconds with 35000 bytes received
A live stream never ends, so curl stops at its time limit; the byte count shows the audio flowing. With the source stopped, the script answered HTTP/1.1 502 Bad Gateway and "The stream is offline." instead of an empty audio response.
Limits to plan for
- One PHP worker per listener. Debian's PHP-FPM pool allows
pm.max_children = 5by default. With five listeners connected, a sixth got nothing:curl: (28) Operation timed out after 6002 milliseconds with 0 bytes received
and PHP-FPM loggedWARNING: [pool www] server reached pm.max_children setting (5), consider raising it. Your other PHP pages share the same workers, so they wait too. Each busy worker used 12,560 KB to 14,736 KB of memory in our test. - Let the script stop itself. Our first version used
ignore_user_abort(false). When a listener left, PHP ended the script in the middle of the curl transfer and the connection to Icecast stayed open: Icecast went on counting a listener who had gone (one run with PHP's built-in server collected four such ghost listeners, still connected about two minutes later when we stopped PHP). Withignore_user_abort(true)and theconnection_aborted()check, the count drops straight back:radio.php http=200 bytes=84000 3 s after leaving radio.php: <Listeners>0</Listeners> radio-old.php http=200 bytes=84000 3 s after leaving radio-old.php: <Listeners>1</Listeners>
(radio-old.phpis the same script withignore_user_abort(false).) - Bandwidth counts twice when the stream runs on another server: your website downloads each listener's copy and sends it out again.
- Shared hosting. Scripts that run for hours may break your host's process or time limits, so check the rules before you rely on this.
Better options
- Centova Cast: the stream proxy serves https tune-in links with negligible load, according to Centova. Enabling it takes one command on the server.
- Your own server: an nginx reverse proxy, or the streaming server's own TLS support. See streaming Shoutcast over HTTPS.
- Ucartz Shoutcast and Icecast hosting: open a support ticket if you need an https stream link for your account.
Official documentation: PHP connection handling, Centova: mixed content warnings and Centova Cast release notes.