File size: 3,960 Bytes
1501522 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | <?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);
}
}
}
|