LiveStreamFrom: Yt - X & Odyssee with cache - PHP Script

Hello everyone,

There have already been requests for this type of script. I got to work, and here is the result. It's not pretentious, but if it helps you, consider visiting the radio station's website and making a donation. Thanks in advance:

Sorry for my English, my native language is French ;-)

P.S. You must have a license in your country to broadcast music (musical copyright, related rights), don't forget!

_____________________________________________________________________

Installation and Usage Guide for the LiveStreamFrom Script

This PHP script retrieves videos from supported platforms (YouTube, Odysee, X), converts them to MP3, caches them, and streams them with ICY metadata.
Prerequisites
Before using this script, ensure the following dependencies are installed on your server:
  1. PHP: Version 7.0 or higher with the following extensions enabled:
    • exec (to run shell commands)
    • fileinfo (for file management)
  2. yt-dlp: Video downloading tool.
    • Download it from the official repository.
    • Place the executable in an accessible directory (e.g., /path/to/your/project/.local/bin/yt-dlp).
    • Make it executable: chmod +x /path/to/yt-dlp.
  3. FFmpeg: Audio/video conversion tool.
    • Install it via your system’s package manager:
      • On Ubuntu/Debian: sudo apt update && sudo apt install ffmpeg
      • On CentOS/RHEL: sudo yum install ffmpeg
    • Or download it from the official site and place it in an accessible directory (e.g., /path/to/your/project/.local/bin/ffmpeg).
  4. Server Dependencies:
    • A web server (Apache, Nginx, etc.) with PHP configured.
    • Write permissions in the directory where temporary files and logs will be stored.

Installation
  1. Directory Structure:
    • Create a working directory on your server (e.g., /path/to/your/project/web/LiveStreamFrom/).
    • Ensure this directory is writable by the web server (e.g., chmod 755 /path/to/LiveStreamFrom).
  2. Placing Dependencies:
    • Place yt-dlp and ffmpeg in an accessible directory (e.g., /path/to/your/project/.local/bin/).
    • Verify their paths in the script (variables $ytDlpPath and $ffmpegPath).
  3. Script Configuration:
    • Copy the PHP script into your working directory (e.g., stream.php).
    • Adjust the constants at the top of the script based on your environment:
      • BASE_PATH: Absolute path to your project’s root directory.
      • WEB_PATH: Subdirectory where temporary files and logs will be stored (e.g., BASE_PATH . '/web/LiveStreamFrom/').
      • LOG_FILE: Location of the log file (e.g., WEB_PATH . 'stream.log').
      • CACHE_EXPIRY: Cache lifetime in seconds (default: 7200s, or 2 hours).
      • CHUNK_SIZE: Size of streaming chunks (default: 8192 bytes).
      • META_INTERVAL: ICY metadata interval (default: 16000 bytes).
  4. Permissions:
    • Ensure the web server has permission to execute yt-dlp and ffmpeg.
    • Grant write permissions to the WEB_PATH directory for caching and logs (e.g., chown www-data:www-data /path/to/LiveStreamFrom on an Apache server).

Usage
  1. Accessing the Script:
    • Call the script via an HTTP GET request with a url parameter containing the video URL:

      Code:
      http://your-domain.com/LiveStreamFrom/stream.php?url=https://<YOUR_VIDEO_ADRESS_HERE>
        • Supported platforms: YouTube, Odysee, X.
      1. How It Works:
        • The script validates the URL and detects the platform.
        • It downloads the video using yt-dlp (prioritizing bestaudio, falling back to best).
        • It converts the file to MP3 using ffmpeg (128 kbps, 44.1 kHz).
        • The MP3 file is cached to avoid re-downloading during the CACHE_EXPIRY period.
        • The result is streamed with ICY metadata (title, artist).
      2. Output:
        • The audio stream is delivered in MP3 format with HTTP headers suitable for streaming (compatible with players like VLC or Winamp).
        • Metadata (title and artist) is embedded in the stream.

    • Troubleshooting
      • Logs: Check the log file (e.g., stream.log) to diagnose issues.
      • Common Errors:
        • 400 Bad Request: Missing URL or unsupported platform.
        • 500 Internal Server Error: Issues with yt-dlp, ffmpeg, or file permissions.
      • Verify that the paths to yt-dlp and ffmpeg are correct and that the tools are executable.
PHP:
<?php
set_time_limit(0);

//CopyRights: Radio Electrons Libres (electronslibres.ch)

// Constants
const BASE_PATH = '/path/to/your/project'; // Replace with your project root path
const WEB_PATH = BASE_PATH . '/web/LiveStreamFrom/';
const LOG_FILE = WEB_PATH . 'stream.log';
const CACHE_EXPIRY = 7200; // 2 hours
const CHUNK_SIZE = 8192;
const META_INTERVAL = 16000;

$ytDlpPath = BASE_PATH . '/.local/bin/yt-dlp'; // Path to yt-dlp executable
$ffmpegPath = BASE_PATH . '/.local/bin/ffmpeg'; // Path to ffmpeg executable

function logMessage(string $message): void {
    file_put_contents(LOG_FILE, "$message\n", FILE_APPEND);
}

logMessage("Script started: " . date('Y-m-d H:i:s'));

// URL validation
$url = $_GET['url'] ?? '';
if (empty($url)) {
    logMessage("Error: Missing URL");
    http_response_code(400);
    exit("A video URL from a supported platform is required");
}
logMessage("Received URL: $url");

// Platform detection
$isYouTube = preg_match('/(youtube\.com|youtu\.be)/i', $url);
$isOdysee = preg_match('/odysee\.com/i', $url);
$isTwitter = preg_match('/(twitter\.com|x\.com)/i', $url);

if (!$isYouTube && !$isOdysee && !$isTwitter) {
    logMessage("Error: Unsupported platform");
    http_response_code(400);
    exit("URL must come from a supported platform (YouTube, Odysee, or X)");
}

$platform = $isYouTube ? "YouTube" : ($isOdysee ? "Odysee" : "X");
logMessage("Detected platform: $platform");

// Cache cleanup
foreach (glob(WEB_PATH . "cache_*") as $file) {
    if (filemtime($file) < time() - CACHE_EXPIRY) {
        @unlink($file);
    }
}

// Temporary file preparation
$cacheKey = md5($url);
$tempFile = WEB_PATH . 'cache_' . $cacheKey;
$tempMp3 = $tempFile . '.mp3';

// Fetching metadata
$metadataCmd = "$ytDlpPath --get-title " . escapeshellarg($url);
$title = trim(shell_exec($metadataCmd)) ?: "$platform Stream";
$artist = $platform;
logMessage("Retrieved title: $title");

// MP3 processing
if (!file_exists($tempMp3) || filesize($tempMp3) === 0) {
    if (!file_exists($tempFile) || filesize($tempFile) === 0) {
        // Try bestaudio first
        $downloadCmd = "$ytDlpPath -f bestaudio --no-playlist -o " . escapeshellarg($tempFile) . " " . escapeshellarg($url) . " 2>&1";
        logMessage("yt-dlp command (bestaudio): $downloadCmd");
        exec($downloadCmd, $output, $returnCode);
        $outputStr = implode("\n", $output);

        // Fallback to best if bestaudio fails
        if ($returnCode !== 0 || !file_exists($tempFile) || filesize($tempFile) === 0) {
            logMessage("bestaudio failed, trying best: $outputStr");
            $downloadCmd = "$ytDlpPath -f best --no-playlist -o " . escapeshellarg($tempFile) . " " . escapeshellarg($url) . " 2>&1";
            logMessage("yt-dlp command (best): $downloadCmd");
            exec($downloadCmd, $output, $returnCode);
            $outputStr = implode("\n", $output);
        }

        if ($returnCode !== 0 || !file_exists($tempFile) || filesize($tempFile) === 0) {
            logMessage("yt-dlp failed or file empty, size: " . (file_exists($tempFile) ? filesize($tempFile) : 'non-existent') . " bytes");
            @unlink($tempFile);
            http_response_code(500);
            exit("Download error: $outputStr");
        }
        logMessage("Download successful: $tempFile, size: " . filesize($tempFile) . " bytes");
    }

    $ffmpegLog = WEB_PATH . 'ffmpeg.log';
    $streamCmd = "$ffmpegPath -i " . escapeshellarg($tempFile) . " -acodec mp3 -ab 128k -ar 44100 -f mp3 " .
                 "-metadata title=" . escapeshellarg($title) . " -metadata artist=" . escapeshellarg($artist) . " " .
                 escapeshellarg($tempMp3) . " 2>$ffmpegLog";
    logMessage("ffmpeg command: $streamCmd");
    exec($streamCmd, $ffmpegOutput, $ffmpegReturnCode);

    if ($ffmpegReturnCode !== 0 || !file_exists($tempMp3) || filesize($tempMp3) === 0) {
        $ffmpegError = file_exists($ffmpegLog) ? file_get_contents($ffmpegLog) : 'Unknown error';
        logMessage("ffmpeg conversion failed: $ffmpegError");
        @unlink($tempFile);
        @unlink($tempMp3);
        http_response_code(500);
        exit("ffmpeg conversion error: $ffmpegError");
    }
    logMessage("Conversion successful: $tempMp3, size: " . filesize($tempMp3) . " bytes");
} else {
    logMessage("MP3 already cached: $tempMp3, size: " . filesize($tempMp3) . " bytes");
}

// HTTP headers
header('Content-Type: audio/mpeg');
header('icy-br: 128');
header('icy-description: Streaming between your ears...');
header('icy-genre: Various');
header('icy-name: Free Stream Radio');
header('icy-pub: 1');
header('icy-url: http://your-domain.com'); // Replace with your domain
header('Cache-Control: no-cache, no-store');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Origin, Accept, X-Requested-With, Content-Type');
header('Access-Control-Allow-Methods: GET, OPTIONS, HEAD');
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('icy-metaint: ' . META_INTERVAL);

// ICY metadata
$metadata = "StreamTitle='$artist - $title';";
$metadataLength = strlen($metadata);
$metadataBlock = chr(ceil($metadataLength / 16)) . $metadata . str_repeat("\0", (16 * ceil($metadataLength / 16)) - $metadataLength);

// File streaming
$fp = @fopen($tempMp3, 'rb');
if ($fp === false) {
    logMessage("Error: Could not open $tempMp3");
    http_response_code(500);
    exit("Server error");
}

$bytesSent = 0;
while (!feof($fp)) {
    $chunkSize = min(META_INTERVAL - $bytesSent, CHUNK_SIZE);
    $buffer = fread($fp, $chunkSize);
    if ($buffer === false) break;
    echo $buffer;
    $bytesSent += strlen($buffer);

    if ($bytesSent >= META_INTERVAL) {
        echo $metadataBlock;
        $bytesSent -= META_INTERVAL;
    }
    flush();
}

fclose($fp);
logMessage("Script ended: " . date('Y-m-d H:i:s'));
?>
 
Last edited:
Hello everyone,

There have already been requests for this type of script. I got to work, and here is the result. It's not pretentious, but if it helps you, consider visiting the radio station's website and making a donation. Thanks in advance:

Sorry for my English, my native language is French ;-)

P.S. You must have a license in your country to broadcast music (musical copyright, related rights), don't forget!

_____________________________________________________________________

Installation and Usage Guide for the LiveStreamFrom Script

This PHP script retrieves videos from supported platforms (YouTube, Odysee, X), converts them to MP3, caches them, and streams them with ICY metadata.
Prerequisites
Before using this script, ensure the following dependencies are installed on your server:
  1. PHP: Version 7.0 or higher with the following extensions enabled:
    • exec (to run shell commands)
    • fileinfo (for file management)
  2. yt-dlp: Video downloading tool.
    • Download it from the official repository.
    • Place the executable in an accessible directory (e.g., /path/to/your/project/.local/bin/yt-dlp).
    • Make it executable: chmod +x /path/to/yt-dlp.
  3. FFmpeg: Audio/video conversion tool.
    • Install it via your system’s package manager:
      • On Ubuntu/Debian: sudo apt update && sudo apt install ffmpeg
      • On CentOS/RHEL: sudo yum install ffmpeg
    • Or download it from the official site and place it in an accessible directory (e.g., /path/to/your/project/.local/bin/ffmpeg).
  4. Server Dependencies:
    • A web server (Apache, Nginx, etc.) with PHP configured.
    • Write permissions in the directory where temporary files and logs will be stored.

Installation
  1. Directory Structure:
    • Create a working directory on your server (e.g., /path/to/your/project/web/LiveStreamFrom/).
    • Ensure this directory is writable by the web server (e.g., chmod 755 /path/to/LiveStreamFrom).
  2. Placing Dependencies:
    • Place yt-dlp and ffmpeg in an accessible directory (e.g., /path/to/your/project/.local/bin/).
    • Verify their paths in the script (variables $ytDlpPath and $ffmpegPath).
  3. Script Configuration:
    • Copy the PHP script into your working directory (e.g., stream.php).
    • Adjust the constants at the top of the script based on your environment:
      • BASE_PATH: Absolute path to your project’s root directory.
      • WEB_PATH: Subdirectory where temporary files and logs will be stored (e.g., BASE_PATH . '/web/LiveStreamFrom/').
      • LOG_FILE: Location of the log file (e.g., WEB_PATH . 'stream.log').
      • CACHE_EXPIRY: Cache lifetime in seconds (default: 7200s, or 2 hours).
      • CHUNK_SIZE: Size of streaming chunks (default: 8192 bytes).
      • META_INTERVAL: ICY metadata interval (default: 16000 bytes).
  4. Permissions:
    • Ensure the web server has permission to execute yt-dlp and ffmpeg.
    • Grant write permissions to the WEB_PATH directory for caching and logs (e.g., chown www-data:www-data /path/to/LiveStreamFrom on an Apache server).

Usage
  1. Accessing the Script:
    • Call the script via an HTTP GET request with a url parameter containing the video URL:

      Code:
      http://your-domain.com/LiveStreamFrom/stream.php?url=https://<YOUR_VIDEO_ADRESS_HERE>
        • Supported platforms: YouTube, Odysee, X.
      1. How It Works:
        • The script validates the URL and detects the platform.
        • It downloads the video using yt-dlp (prioritizing bestaudio, falling back to best).
        • It converts the file to MP3 using ffmpeg (128 kbps, 44.1 kHz).
        • The MP3 file is cached to avoid re-downloading during the CACHE_EXPIRY period.
        • The result is streamed with ICY metadata (title, artist).
      2. Output:
        • The audio stream is delivered in MP3 format with HTTP headers suitable for streaming (compatible with players like VLC or Winamp).
        • Metadata (title and artist) is embedded in the stream.

    • Troubleshooting
      • Logs: Check the log file (e.g., stream.log) to diagnose issues.
      • Common Errors:
        • 400 Bad Request: Missing URL or unsupported platform.
        • 500 Internal Server Error: Issues with yt-dlp, ffmpeg, or file permissions.
      • Verify that the paths to yt-dlp and ffmpeg are correct and that the tools are executable.
PHP:
<?php
set_time_limit(0);

//CopyRights: Radio Electrons Libres (electronslibres.ch)

// Constants
const BASE_PATH = '/path/to/your/project'; // Replace with your project root path
const WEB_PATH = BASE_PATH . '/web/LiveStreamFrom/';
const LOG_FILE = WEB_PATH . 'stream.log';
const CACHE_EXPIRY = 7200; // 2 hours
const CHUNK_SIZE = 8192;
const META_INTERVAL = 16000;

$ytDlpPath = BASE_PATH . '/.local/bin/yt-dlp'; // Path to yt-dlp executable
$ffmpegPath = BASE_PATH . '/.local/bin/ffmpeg'; // Path to ffmpeg executable

function logMessage(string $message): void {
    file_put_contents(LOG_FILE, "$message\n", FILE_APPEND);
}

logMessage("Script started: " . date('Y-m-d H:i:s'));

// URL validation
$url = $_GET['url'] ?? '';
if (empty($url)) {
    logMessage("Error: Missing URL");
    http_response_code(400);
    exit("A video URL from a supported platform is required");
}
logMessage("Received URL: $url");

// Platform detection
$isYouTube = preg_match('/(youtube\.com|youtu\.be)/i', $url);
$isOdysee = preg_match('/odysee\.com/i', $url);
$isTwitter = preg_match('/(twitter\.com|x\.com)/i', $url);

if (!$isYouTube && !$isOdysee && !$isTwitter) {
    logMessage("Error: Unsupported platform");
    http_response_code(400);
    exit("URL must come from a supported platform (YouTube, Odysee, or X)");
}

$platform = $isYouTube ? "YouTube" : ($isOdysee ? "Odysee" : "X");
logMessage("Detected platform: $platform");

// Cache cleanup
foreach (glob(WEB_PATH . "cache_*") as $file) {
    if (filemtime($file) < time() - CACHE_EXPIRY) {
        @unlink($file);
    }
}

// Temporary file preparation
$cacheKey = md5($url);
$tempFile = WEB_PATH . 'cache_' . $cacheKey;
$tempMp3 = $tempFile . '.mp3';

// Fetching metadata
$metadataCmd = "$ytDlpPath --get-title " . escapeshellarg($url);
$title = trim(shell_exec($metadataCmd)) ?: "$platform Stream";
$artist = $platform;
logMessage("Retrieved title: $title");

// MP3 processing
if (!file_exists($tempMp3) || filesize($tempMp3) === 0) {
    if (!file_exists($tempFile) || filesize($tempFile) === 0) {
        // Try bestaudio first
        $downloadCmd = "$ytDlpPath -f bestaudio --no-playlist -o " . escapeshellarg($tempFile) . " " . escapeshellarg($url) . " 2>&1";
        logMessage("yt-dlp command (bestaudio): $downloadCmd");
        exec($downloadCmd, $output, $returnCode);
        $outputStr = implode("\n", $output);

        // Fallback to best if bestaudio fails
        if ($returnCode !== 0 || !file_exists($tempFile) || filesize($tempFile) === 0) {
            logMessage("bestaudio failed, trying best: $outputStr");
            $downloadCmd = "$ytDlpPath -f best --no-playlist -o " . escapeshellarg($tempFile) . " " . escapeshellarg($url) . " 2>&1";
            logMessage("yt-dlp command (best): $downloadCmd");
            exec($downloadCmd, $output, $returnCode);
            $outputStr = implode("\n", $output);
        }

        if ($returnCode !== 0 || !file_exists($tempFile) || filesize($tempFile) === 0) {
            logMessage("yt-dlp failed or file empty, size: " . (file_exists($tempFile) ? filesize($tempFile) : 'non-existent') . " bytes");
            @unlink($tempFile);
            http_response_code(500);
            exit("Download error: $outputStr");
        }
        logMessage("Download successful: $tempFile, size: " . filesize($tempFile) . " bytes");
    }

    $ffmpegLog = WEB_PATH . 'ffmpeg.log';
    $streamCmd = "$ffmpegPath -i " . escapeshellarg($tempFile) . " -acodec mp3 -ab 128k -ar 44100 -f mp3 " .
                 "-metadata title=" . escapeshellarg($title) . " -metadata artist=" . escapeshellarg($artist) . " " .
                 escapeshellarg($tempMp3) . " 2>$ffmpegLog";
    logMessage("ffmpeg command: $streamCmd");
    exec($streamCmd, $ffmpegOutput, $ffmpegReturnCode);

    if ($ffmpegReturnCode !== 0 || !file_exists($tempMp3) || filesize($tempMp3) === 0) {
        $ffmpegError = file_exists($ffmpegLog) ? file_get_contents($ffmpegLog) : 'Unknown error';
        logMessage("ffmpeg conversion failed: $ffmpegError");
        @unlink($tempFile);
        @unlink($tempMp3);
        http_response_code(500);
        exit("ffmpeg conversion error: $ffmpegError");
    }
    logMessage("Conversion successful: $tempMp3, size: " . filesize($tempMp3) . " bytes");
} else {
    logMessage("MP3 already cached: $tempMp3, size: " . filesize($tempMp3) . " bytes");
}

// HTTP headers
header('Content-Type: audio/mpeg');
header('icy-br: 128');
header('icy-description: Streaming between your ears...');
header('icy-genre: Various');
header('icy-name: Free Stream Radio');
header('icy-pub: 1');
header('icy-url: http://your-domain.com'); // Replace with your domain
header('Cache-Control: no-cache, no-store');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Origin, Accept, X-Requested-With, Content-Type');
header('Access-Control-Allow-Methods: GET, OPTIONS, HEAD');
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('icy-metaint: ' . META_INTERVAL);

// ICY metadata
$metadata = "StreamTitle='$artist - $title';";
$metadataLength = strlen($metadata);
$metadataBlock = chr(ceil($metadataLength / 16)) . $metadata . str_repeat("\0", (16 * ceil($metadataLength / 16)) - $metadataLength);

// File streaming
$fp = @fopen($tempMp3, 'rb');
if ($fp === false) {
    logMessage("Error: Could not open $tempMp3");
    http_response_code(500);
    exit("Server error");
}

$bytesSent = 0;
while (!feof($fp)) {
    $chunkSize = min(META_INTERVAL - $bytesSent, CHUNK_SIZE);
    $buffer = fread($fp, $chunkSize);
    if ($buffer === false) break;
    echo $buffer;
    $bytesSent += strlen($buffer);

    if ($bytesSent >= META_INTERVAL) {
        echo $metadataBlock;
        $bytesSent -= META_INTERVAL;
    }
    flush();
}

fclose($fp);
logMessage("Script ended: " . date('Y-m-d H:i:s'));
?>
P.S 2 I tried to include the coverart but impossible RadioBoss does not take it, even when trying to inject it via API, if you find a solution for this, please let us know
 
Back
Top