teknolis / app /Services /VideoMusicComposerService.php
OpenAI Codex
Fix storage symlink and adapt cron status for Spaces
1501522
Raw
History Blame Contribute Delete
3.96 kB
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
class VideoMusicComposerService
{
public function compose(string $videoUrl, string $musicUrl, float $musicVolume = 0.24): array
{
$videoPath = $this->resolveToLocalPath($videoUrl, 'video');
$musicPath = $this->resolveToLocalPath($musicUrl, 'audio');
$musicVolume = max(0.05, min(1.0, $musicVolume));
$outputRelative = 'videos/social_mix_'.now()->format('Ymd_His').'_'.Str::lower(Str::random(8)).'.mp4';
$outputPath = Storage::disk('public')->path($outputRelative);
$this->ensureDirectory(dirname($outputPath));
$mixCommand = sprintf(
'ffmpeg -y -i %s -stream_loop -1 -i %s -filter_complex %s -map 0:v:0 -map [aout] -c:v copy -c:a aac -shortest %s 2>&1',
escapeshellarg($videoPath),
escapeshellarg($musicPath),
escapeshellarg('[0:a]volume=1.0[a0];[1:a]volume='.$musicVolume.'[a1];[a0][a1]amix=inputs=2:duration=first:dropout_transition=2[aout]'),
escapeshellarg($outputPath)
);
exec($mixCommand, $mixOutput, $mixCode);
if ($mixCode !== 0) {
$replaceCommand = sprintf(
'ffmpeg -y -i %s -stream_loop -1 -i %s -filter_complex %s -map 0:v:0 -map [bgm] -c:v copy -c:a aac -shortest %s 2>&1',
escapeshellarg($videoPath),
escapeshellarg($musicPath),
escapeshellarg('[1:a]volume='.$musicVolume.'[bgm]'),
escapeshellarg($outputPath)
);
exec($replaceCommand, $replaceOutput, $replaceCode);
if ($replaceCode !== 0) {
throw new RuntimeException('Gagal merge musik ke video via ffmpeg: '.trim(implode("\n", array_slice($replaceOutput, -8))));
}
}
if (! is_file($outputPath) || filesize($outputPath) < 1024) {
throw new RuntimeException('Output video hasil merge tidak valid.');
}
return [
'url' => Storage::url($outputRelative),
'relative_path' => $outputRelative,
'music_volume' => $musicVolume,
];
}
private function resolveToLocalPath(string $source, string $kind): string
{
$source = trim($source);
if ($source === '') {
throw new RuntimeException('URL '.$kind.' kosong.');
}
if (Str::startsWith($source, '/storage/')) {
$relative = ltrim(Str::replaceFirst('/storage/', '', $source), '/');
$path = Storage::disk('public')->path($relative);
if (! is_file($path)) {
throw new RuntimeException('File '.$kind.' lokal tidak ditemukan: '.$source);
}
return $path;
}
if (! Str::startsWith($source, ['http://', 'https://'])) {
throw new RuntimeException('URL '.$kind.' harus http/https atau /storage/...');
}
$ext = $kind === 'audio' ? 'mp3' : 'mp4';
$relativePath = 'tmp/social_media/'.$kind.'_'.now()->format('Ymd_His').'_'.Str::lower(Str::random(8)).'.'.$ext;
$fullPath = Storage::disk('local')->path($relativePath);
$this->ensureDirectory(dirname($fullPath));
$response = Http::connectTimeout(20)
->timeout(180)
->get($source);
if (! $response->successful()) {
throw new RuntimeException('Gagal download '.$kind.': '.$response->status());
}
file_put_contents($fullPath, $response->body());
if (! is_file($fullPath) || filesize($fullPath) < 512) {
throw new RuntimeException('File '.$kind.' hasil download tidak valid.');
}
return $fullPath;
}
private function ensureDirectory(string $path): void
{
if (! is_dir($path)) {
mkdir($path, 0775, true);
}
}
}