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