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); } } }