ActivityMakeMediaJob.php 8.8 KB
<?php

namespace App\Jobs;

use App\Enums\ActivityStatusEnum;
use App\Helpers\UploadHelper;
use App\Models\Activity;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use OSS\OssClient;
use ProtoneMedia\LaravelFFMpeg\Support\FFMpeg;

class ActivityMakeMediaJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public Activity $activity;

    public int $toStatus;

    public bool $buildMedia;

    public int $timeout = 20 * 60;

    public int $tries = 3;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(Activity $activity, ActivityStatusEnum $toStatus, bool $buildMedia = false)
    {
        $this->activity   = $activity;
        $this->toStatus   = $toStatus->value;
        $this->buildMedia = $buildMedia;
    }

    /**
     * 获取应该分配给任务的标记
     *
     * @return array<int, string>
     */
    public function tags(): array
    {
        return ['activity', 'make-media:' . $this->activity->getKey()];
    }

    /**
     * @param string $key
     * @return string
     */
    protected function getSourceUrl(string $key): string
    {
        return Arr::get($this->activity, 'expand.' . $key . '.url', '');
    }

    /**
     * Execute the job.
     *
     * @return void
     * @throws \OSS\Core\OssException
     */
    public function handle(): void
    {
        try {
            DB::beginTransaction();
            $this->backup();

            $attribute = ['status' => $this->toStatus];
            $clipTime  = '';

            if ($this->activity->getAttribute('is_lyric') === 1 && $this->activity->getAttribute('lyric') && $this->activity->getAttribute('clip_lyric')) {
                $clipTime = $this->getClipTime($this->activity->getAttribute('lyric'), $this->activity->getAttribute('clip_lyric'));
            }

            if ($this->getSourceUrl('guide_source')) {
                $guideName                   = Str::uuid()->toString();
                $guide                       = $this->downloadSourceFile('guide_source', $guideName);
                $attribute['guide']          = $this->uploadOss($guide);
                $attribute['guide_duration'] = $this->getFileDurationInSecond($guide);

                if (!empty($clipTime)) {
                    $guideClip                        = $this->clipAudio($guide, $guideName, ...$clipTime);
                    $attribute['guide_clip']          = $this->uploadOss($guideClip);
                    $attribute['guide_clip_duration'] = $this->getFileDurationInSecond($guideClip);
                }
            }

            if ($this->getSourceUrl('karaoke_source')) {
                $karaokeName          = Str::uuid()->toString();
                $karaoke              = $this->downloadSourceFile('karaoke_source', $karaokeName);
                $attribute['karaoke'] = $this->uploadOss($karaoke);

                if (!empty($clipTime)) {
                    $karaokeClip               = $this->clipAudio($karaoke, $karaokeName, ...$clipTime);
                    $attribute['karaoke_clip'] = $this->uploadOss($karaokeClip);
                }

            }

            Activity::query()->whereKey($this->activity->getKey())->update($attribute);

            DB::commit();
        } catch (Exception $exception) {
            DB::rollBack();
            Log::error(__CLASS__, ['activity_id' => $this->activity->getKey(), 'message' => $exception->getMessage()]);
            Activity::query()->whereKey($this->activity->getKey())->update(['status' => 4]);
        }

        FFMpeg::cleanupTemporaryFiles();
        Storage::disk('make')->deleteDirectory($this->activity->getKey());
    }

    /**
     * @param string $key
     * @param string $name
     * @return string
     */
    protected function downloadSourceFile(string $key, string $name): string
    {
        $path = $this->activity->getKey() . '/' . $name . '.m4a';
        $url  = UploadHelper::toEndpointUrl($this->getSourceUrl($key), UploadHelper::isInternalServer());
        FFMpeg::openUrl($url)->export()->addFilter(['-ar', '44100', '-c:a', 'libfdk_aac', '-vn'])->toDisk('make')->save($path);
        return $path;
    }

    /**
     * @param string      $sourcePath
     * @param string      $name
     * @param string|null $start
     * @param string|null $end
     * @return string
     */
    protected function clipAudio(string $sourcePath, string $name, ?string $start, ?string $end): string
    {
        $path      = $this->activity->getKey() . '/' . 'clip_' . $name . '.m4a';
        $tmpPath   = $this->activity->getKey() . '/' . Str::uuid()->toString() . '.m4a';
        $clipStart = $this->durationToSecond($start);
        $clipEnd   = is_null($end) ? [] : ['-t', ($this->durationToSecond($end) - (max($clipStart - 5, 0)))];
        $clipStart = $clipStart - 5 <= 0 ? '00:00:00.000' : $this->secondToDuration($clipStart - 5);

        FFMpeg::fromDisk('make')->open($sourcePath)->export()->addFilter(array_filter(['-ss', $clipStart, ...$clipEnd, '-acodec', 'copy']))->toDisk('make')->save($tmpPath);
        FFMpeg::fromDisk('make')->open($tmpPath)->export()->addFilter(['-ar', '44100', '-c:a', 'libfdk_aac', '-vn'])->toDisk('make')->save($path);

        return $path;
    }

    /**
     * @param string $lyric
     * @param string $clipLyric
     * @return array
     */
    protected function getClipTime(string $lyric, string $clipLyric): array
    {
        $preg = '/\[\d*:\d*\.\d*]/';
        preg_match_all($preg, $lyric, $lyricArr);
        preg_match_all($preg, $clipLyric, $clipLyricArr);
        $lyricArr     = array_shift($lyricArr);
        $clipLyricArr = array_shift($clipLyricArr);
        $clipLyricEnd = end($clipLyricArr);
        $sourceIndex  = array_search($clipLyricEnd, $lyricArr, true);

        $startTime = str_replace(['[', ']'], '', array_shift($clipLyricArr));
        if ($sourceIndex && isset($lyricArr[$sourceIndex + 1])) {
            $endTime = str_replace(['[', ']'], '', $lyricArr[$sourceIndex + 1]);
        } else {
            $endTime = NULL;
        }
        return [$startTime, $endTime];
    }

    /**
     * @param $path
     * @return string
     */
    public function getMakeFilePath($path): string
    {
        return Storage::disk('make')->path($path);
    }

    /**
     * @param string $path
     * @return int
     */
    public function getFileDurationInSecond(string $path): int
    {
        return FFMpeg::fromDisk('make')->open($path)->getDurationInSeconds();
    }

    /**
     * @param string|null $duration
     * @return float
     */
    protected function durationToSecond(?string $duration): float
    {
        $time = date_parse($this->durationFormat($duration));

        return ((int)$time['hour'] * 3600) + ((int)$time['minute'] * 60) + (int)$time['second'] + (float)$time['fraction'];
    }

    /**
     * @param float $second
     * @return string
     */
    protected function secondToDuration(float $second): string
    {
        $arr = explode('.', $second);

        if (count($arr) === 1) {
            $arr[] = 0;
        }

        return gmdate('H:i:s', $arr[0]) . '.' . end($arr);
    }

    /**
     * @param string|null $duration
     * @return string
     */
    protected function durationFormat(?string $duration): string
    {
        if (is_null($duration)) {
            return '00:00:00.000';
        }

        $arr = explode(':', $duration);

        if (count($arr) === 1) {
            array_unshift($arr, '00');
        }

        if (count($arr) === 2) {
            array_unshift($arr, '00');
        }

        return implode(':', $arr);
    }

    /**
     * @param $file
     * @return string
     * @throws \OSS\Core\OssException
     */
    protected function uploadOss($file): string
    {
        return UploadHelper::put(
            'make/' . now()->format('Ymd') . '/' . Str::afterLast($file, '/'),
            $this->getMakeFilePath($file),
            [OssClient::OSS_CONTENT_TYPE => 'audio/mp4']
        );
    }

    /**
     * @return bool
     */
    protected function backup(): bool
    {
        $guide   = $this->activity->getAttribute('guide');
        $karaoke = $this->activity->getAttribute('karaoke');

        if ($guide && $karaoke) {
            DB::table('activity_audios')->insert([
                'activity_id' => $this->activity->getKey(),
                'guide'       => $this->activity->getAttribute('guide'),
                'karaoke'     => $this->activity->getAttribute('karaoke'),
                'created_at'  => Carbon::now()->toDateTimeString()
            ]);
        }

        return true;
    }
}