Stream video với ffmpeg và laravel
Mình xử dụng https://github.com/PHP-FFMpeg/PHP-FFMpeg để tạo file hls video nhằm mục đích stream
<?php
namespace App\Services;
use FFMpeg\Format\Video\X264;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;
class FfmpegService
{
protected $ffmpeg;
// lỗi Unable to load FFProbe
/*
sudo apt update
sudo apt install ffmpeg -y
Kiểm tra:
which ffmpeg
which ffprobe cho vào env
*/
public function __construct()
{
// Cấu hình FFmpeg và FFProbe
$this->ffmpeg = \FFMpeg\FFMpeg::create([
'ffmpeg.binaries' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'),
'ffprobe.binaries' => env('FFPROBE_PATH', '/usr/bin/ffprobe'),
'timeout' => 0, // giây
]);
// $this->ffmpeg = \FFMpeg\FFMpeg::create([
// 'ffmpeg.binaries' => env('FFMPEG_PATH', 'C:\\ffmpeg\\bin\\ffmpeg.exe'),
// 'ffprobe.binaries' => env('FFPROBE_PATH', 'C:\\ffmpeg\\bin\\ffprobe.exe'),
// 'timeout' => 0, // giây
// ]);
}
/**
* Chuyển video sang HLS
*/
public function convertToHLS(string $diskInput, string $inputPath, string $diskOutput, string $outputDir, bool $useGpu = false): array
{
$filename = basename($inputPath);
$tempFile = storage_path('app/temp_video_' . $filename);
$hlsDir = storage_path('app/hls_' .Str::slug( pathinfo($inputPath, PATHINFO_FILENAME)));
try {
// 1️⃣ Copy file từ disk input sang local
$stream = Storage::disk($diskInput)->readStream($inputPath);
if (!$stream) {
throw new \Exception("Không thể đọc file nguồn");
}
$out = fopen($tempFile, 'w');
while (!feof($stream)) {
fwrite($out, fread($stream, 8 * 1024 * 1024)); // đọc 8MB/lần
}
fclose($out);
fclose($stream);
// // 2️⃣ Chuẩn bị thư mục output HLS local
// if (!is_dir($hlsDir)) {
// mkdir($hlsDir, 0755, true);
// }
// Nếu folder tồn tại, xóa hết
if (is_dir($hlsDir)) {
File::deleteDirectory($hlsDir);
}
// Tạo lại folder mới
mkdir($hlsDir, 0755, true);
// 3️⃣ Chạy FFmpeg tạo HLS
$video = $this->ffmpeg->open($tempFile);
/*
$format = new X264('aac', 'libx264');
$format->setAdditionalParameters([
'-profile:v', 'baseline',
'-level', '3.0',
'-start_number', '0',
'-hls_time', '10',
'-hls_list_size', '0',
'-hls_segment_filename', $hlsDir . '/segment_%03d.ts',
]);
*/
if ($useGpu) {
// GPU NVIDIA NVENC
$format = new X264('aac', 'h264_nvenc');
$format->setAdditionalParameters([
'-preset', 'fast',
'-crf', '23',
'-b:v', '5M',
'-maxrate', '5M',
'-bufsize', '10M',
'-hls_time', '10',
'-hls_list_size', '0',
'-hls_segment_filename', $hlsDir . '/segment_%03d.ts',
]);
} else {
// CPU
$format = new X264('aac', 'libx264');
$format->setAdditionalParameters([
'-preset', 'fast',
'-crf', '23',
'-hls_time', '10',
'-hls_list_size', '0',
'-hls_segment_filename', $hlsDir . '/segment_%03d.ts',
]);
}
$outputPath = $hlsDir . '/index.m3u8';
$video->save($format, $outputPath);
// 4️⃣ Upload tất cả file HLS sang disk output
$uploaded = [];
foreach (scandir($hlsDir) as $file) {
if ($file === '.' || $file === '..') continue;
$localPath = $hlsDir . '/' . $file;
$remotePath = trim($outputDir, '/') . '/' . $file;
Storage::disk($diskOutput)->put($remotePath, fopen($localPath, 'r'));
$uploaded[] = $remotePath;
}
// 5️⃣ Dọn dẹp file tạm
@unlink($tempFile);
File::deleteDirectory($hlsDir);
Log::info($hlsDir);
return [
'status' => 1,
'url' => $outputDir . '/index.m3u8',
'files' => $uploaded,
];
} catch (\FFMpeg\Exception\RuntimeException $e) {
dd($e->getMessage(), $e);
// Dọn dẹp tất cả file tạm nếu lỗi
@unlink($tempFile);
if (is_dir($hlsDir)) {
\File::deleteDirectory($hlsDir);
}
return [
'status' => 0,
'error' => $e->getMessage(),
];
}
}
/**
* Chuyển video sang HLS và tạo thư mục output tự động
*/
public function convertToHLSWithAutoDir(string $diskInput, string $inputPath, string $diskOutput): array
{
$filename = pathinfo($inputPath, PATHINFO_FILENAME);
$slug = Str::slug($filename);
$outputDir = "hlsdata/" . $slug;
set_time_limit(400);
return $this->convertToHLS($diskInput, $inputPath, $diskOutput, $outputDir , false);
}
}