PHP has no native audio processing — video/audio conversion means FFmpeg. You can call it via shell_exec (simple, works everywhere FFmpeg is installed) or use the php-ffmpeg wrapper for a more object-oriented API. For environments where you can't install FFmpeg (shared hosting, serverless), the API is the right call.
Method 1: FFmpeg via shell_exec (direct, full control)
The simplest approach — call FFmpeg directly. One line per conversion, full access to every FFmpeg option.
# Check FFmpeg is available
ffmpeg -version | head -1
# Install on Ubuntu/Debian
apt install ffmpeg&1";
$output = shell_exec($cmd);
if (!file_exists($outPath) || filesize($outPath) === 0) {
throw new RuntimeException("FFmpeg failed: " . substr($output, -500));
}
return $outPath;
}
function mp4ToMp3Cbr(string $inPath, string $outPath, int $bitrate = 192): string {
// Fixed bitrate (CBR) — predictable file size
$in = escapeshellarg($inPath);
$out = escapeshellarg($outPath);
$cmd = "ffmpeg -y -i {$in} -vn -acodec libmp3lame -b:a {$bitrate}k {$out} 2>&1";
$output = shell_exec($cmd);
if (!file_exists($outPath) || filesize($outPath) === 0) {
throw new RuntimeException("FFmpeg failed: " . substr($output, -500));
}
return $outPath;
}
// VBR ~190kbps (best quality/size balance)
mp4ToMp3('video.mp4', 'audio.mp3');
// Fixed 128kbps (for speech, podcasts)
mp4ToMp3Cbr('video.mp4', 'speech.mp3', 128);
Quality guide for VBR (-q:a):
- 0-1: ~245kbps — highest quality, largest files. Use for music.
- 2: ~190kbps — transparent quality for most content. Good default.
- 4-5: ~130kbps — noticeably lower quality but fine for speech.
- 8-9: ~65kbps — low quality, small files. Voice memos only.
Method 2: php-ffmpeg wrapper (object-oriented API)
php-ffmpeg wraps the FFmpeg binary in a PHP object model. Good for complex workflows with progress callbacks and format negotiation.
composer require php-ffmpeg/php-ffmpeg '/usr/bin/ffmpeg',
'ffprobe.binaries' => '/usr/bin/ffprobe',
'timeout' => 300,
'ffmpeg.threads' => 2,
]);
$video = $ffmpeg->open($inPath);
$format = new Mp3();
$format->setAudioKiloBitrate($kilobitrate);
$format->setAudioChannels(2);
// Optional: progress callback
$format->on('progress', function($video, $format, $percentage) {
echo "Progress: {$percentage}%\n";
});
$video->save($format, $outPath);
}
mp4ToMp3PhpFfmpeg('video.mp4', 'audio.mp3', 192);
echo "Done\n";
php-ffmpeg wraps FFmpeg — you still need the binary installed. The main advantage over shell_exec is type safety, progress events, and automatic binary detection. For simple one-off conversions, shell_exec is less overhead.
Method 3: ChangeThisFile API (no FFmpeg install)
The API runs FFmpeg server-side. Source format is auto-detected from the filename — just send the file and target=mp3. Free tier: 1,000 conversions/month, no card required.
true,
CURLOPT_TIMEOUT => 300, // large videos may take time
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'file' => new CURLFile($inPath, 'video/mp4', basename($inPath)),
'target' => 'mp3',
],
]);
$result = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
throw new RuntimeException("API error: HTTP {$code}: " . substr($result, 0, 200));
}
file_put_contents($outPath, $result);
}
mp4ToMp3Api('video.mp4', 'audio.mp3', 'ctf_sk_your_key_here');
echo "Done\n";
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| FFmpeg shell_exec | Servers with FFmpeg installed, batch processing | shell_exec must be enabled; FFmpeg binary required |
| php-ffmpeg | Complex workflows, progress tracking, type safety | Composer required; still needs FFmpeg binary |
| ChangeThisFile API | Shared hosting, serverless, no FFmpeg available | Network call; 25MB file limit on free tier (upgrade for larger) |
Production tips
- Use -y flag to overwrite existing output. Without -y, FFmpeg waits for interactive confirmation — it hangs indefinitely when called from PHP.
- Validate output exists and has non-zero size. FFmpeg sometimes exits 0 even when conversion fails. Always check file_exists() and filesize() on the output.
- Set PHP execution timeout. Large video conversions take minutes. Use set_time_limit(0) and also ensure your web server (nginx, Apache) has a matching proxy timeout.
- Check for shell_exec availability. Some shared hosts disable shell_exec. Test with: $check = function_exists('shell_exec') && !in_array('shell_exec', explode(',', ini_get('disable_functions')));
- Use proc_open for better error handling. shell_exec mixes stdout and stderr with 2>&1. proc_open() gives separate handles so you can capture errors without polluting the output stream.
Shell_exec with FFmpeg is the fastest path on any server where FFmpeg is available. php-ffmpeg is worth adding for complex pipelines with progress tracking. For environments where you can't install FFmpeg, the API handles it server-side. Free tier: 1,000 conversions/month.