Skip to content

PHP Proxy to Re-stream an Icecast or Shoutcast Radio Stream on HTTPS (Tested Script)

Updated 5 min read

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

  1. Save the code below as radio.php on your HTTPS website. It needs PHP's curl extension (php -m lists curl when it is installed).
  2. Set STREAM_URL to 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.
  3. Use https://www.example.com/radio.php as 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 = 5 by default. With five listeners connected, a sixth got nothing:
    curl: (28) Operation timed out after 6002 milliseconds with 0 bytes received
    and PHP-FPM logged WARNING: [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). With ignore_user_abort(true) and the connection_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.php is the same script with ignore_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.

Was this answer helpful?

Still stuck? Open a ticket