Budowa platformy video za pomocą FFMPEG - Laravel

Zawsze zastanawiałem się jak ładować wideo asynchronicznie w mniejszych kawałkach. Kilka razy chciałem nawet dodać taki materiał do strony internetowej, jednak zawsze kończyło się to tak samo (wideo ładowało się zaskakująco długo).

Po zgłębieniu tematu dowiedziałem się o istnieniu oprogramowania jakim jest FFMPEG, oraz o paczce stworzonej przez protonemedia w celu integracji z Laravelem

Rozpocznijmy więc

Stworzenie tej prostej aplikacji można podzielić na 3 podpunkty

  1. Instalacja zależności
  2. Stworzenie modelów oraz migracji
  3. Stworzenie kolejki Zainstaluj paczkę:
 composer require pbmedia/laravel-ffmpeg

Stwórz model:

php artisan make:model Video -m

W pliku migracji:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('videos', function (Blueprint $table) {
            $table->id();
            
            // file info
            $table->string('name');
            $table->string('path')->nullable();
            $table->string('progress')->nullable();

            // video info
            $table->string('title')->nullable();
            $table->text('description')->nullable();
            $table->string('tags')->nullable();
            $table->boolean('public');

            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('videos');
    }
};

Stwórz controller:

php artisan make:controller VideoController --api

Następnie musimy napisać funkcję która potnie nasze video i zachowa je w dwóch formatach: stantadrowy, oraz playlista ffmpeg.

public function store (Request $request) {
    $name = Str::random(16);
    $path = $name . '.' . $request->video->getClientOriginalExtension();
    $request->video->storeAs('public', $path);

    $video = new Video;
    $video->name = $name;
    $video->path = asset('/storage/' . $name . '.m3u8');
    $video->progress = 0;
    $video->save();

    VideoProcess::dispatch($path, $name, $video);

    return redirect()->route('video-processing', ['id' => $video->id]);
}

Z powodu długiego czasu przetwarzania controller odnosi się do kolejki, zatem po standardowej konfiguracji możemy dodać odpowiednie pliki:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use FFMpeg\Format\Video\X264;
use App\Models\Video;
use FFMpeg;
use FFMpeg\Format\ProgressListener\AbstractProgressListener;
use ProtoneMedia\LaravelFFMpeg\FFMpeg\ProgressListenerDecorator;

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

    public $path;
    public $name;
    public $video;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($path, $name, Video $video)
    {
        $this->path = $path;
        $this->name = $name;
        $this->video = $video;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $format = new \FFMpeg\Format\Video\X264;
        $decoratedFormat = ProgressListenerDecorator::decorate($format);

        $lowBitrate = (new X264)->setKiloBitrate(250);
        $midBitrate = (new X264)->setKiloBitrate(500);
        $highBitrate = (new X264)->setKiloBitrate(1000);

        FFMpeg::fromDisk('public')
            ->open($this->path)
            ->exportForHLS()
            ->onProgress(function ($percentage) {
                $this->video->progress = $percentage;
                $this->video->save();
            })
            ->inFormat($decoratedFormat)
            ->setSegmentLength(10) // optional
            ->setKeyFrameInterval(48) // optional
            ->addFormat($lowBitrate)
            ->addFormat($midBitrate)
            ->addFormat($highBitrate)
            ->save( $this->name . '.m3u8');

        $this->video->progress = 100;
        $this->video->save();


    }
}

frontend

W tym artykule nie poruszyłem kwestji frontendu, jednak należy pamiętać, że zwykły element video nie jest kompatybilny z playlistami FFMPEG. Jesteśmy zatem zmuszeni używać javascriptowych bibliotek. Video.js jest tu najbardziej popularnym wyborem.