Platforma video z AWS Elastic Transcoder i Amazon S3 – tutorial
Z roku na rok formaty video generują coraz większy % ruchu internetowego. Według CISCO, w 2020 roku było to aż 82% całego ruchu, a prognozy mówią, że trend ten cały czas będzie się nasilał. W tym artykule zajmiemy się problematyką platform video oraz stworzymy prostą stronę w Laravel 8.
Michał Putkowski. Laravel developer w Polcode. W wolnej chwili angażuje się w projekty open-source oraz rozwój frameworka Laravel. Lubi proste rozwiązania oraz czysty kod. Interesuje się sportem oraz nowinkami technologicznymi.
Na jakie problemy możemy natrafić podczas tworzenia własnej platformy video?
- Konwertowanie filmów
Bardzo często pliki wgrywane przez użytkowników nie są odpowiednio zoptymalizowane – mają za duży rozmiar lub w przypadku zdjęć i filmów, są w zbyt wysokiej rozdzielczości, której nie zamierzamy obsługiwać. Warto pomyśleć o konwertowaniu filmów na format, który pozwoli nam zaoszczędzić miejsce na dysku. W tym artykule wykorzystamy format M3U8, który jest podstawą protokołu HLS, stworzonego przez firmę Apple. Zaletami tego rozwiązania jest możliwość płynnego przełączania się pomiędzy dostępnymi jakościami filmu (jeśli są dostępne) oraz rozbicie pliku video na kilka lub kilkadziesiąt mniejszych plików (tzw. video chunks), co znacząco wpływa na wydajność i płynność oglądania.
- Przepustowość serwera
W przypadku platform video, przepustowość odgrywa kluczową rolę w celu zapewnienia płynnego oglądania wielu użytkownikom w tym samym czasie. Dobrym pomysłem może być rozłożenie ruchu na kilka serwerów.
- Pojemność serwera
Filmy zwykle zajmują dużo miejsca na dysku, dlatego jeśli decydujemy się hostować je na własnym serwerze, musimy dobrać odpowiednią powierzchnię dyskową. Zbyt duża pojemność dysku/dysków może skutkować zbędnymi kosztami, jeśli nie będziemy jej w odpowiedni sposób wykorzystywać. Natomiast zbyt mała pojemność uniemożliwi dodawanie większej ilości filmów.
Spis treści
Co to jest AWS S3, Elastic Trancoder i IAM?
Konfiguracja usług na AWS
Zaczniemy od skonfigurowania wszystkich potrzebnych usług na AWS, krok po kroku.
W przypadku usług S3 oraz Elastic Transcoder, będziemy musieli znaleźć wspólny region dla obydwu usług, żeby nie ponosić dodatkowych kosztów za transfer poza region. Dla nas, najlepszym wyborem będzie eu-west-1 (Irlandia), ponieważ jest to jedyny region w Europie, który posiada wsparcie dla usługi Elastic Transcoder.
Amazon S3
S3 jest to usługa typu cloud storage (przechowywanie danych w chmurze). Skalowalność tej usługi daje ogromne możliwości przechowywania danych, jest to praktycznie nielimitowana przestrzeń dyskowa w chmurze. Dodatkowo użytkownik płaci wyłącznie za przestrzeń, którą wykorzystuje (per GB) oraz za transfer według cennika (https://aws.amazon.com/s3/pricing/).
Tworzymy dwa buckety, pierwszy będzie odpowiadał za przechowywanie plików wgranych przez użytkowników (pliki do konwersji), a na drugim będą znajdować się przetworzone filmy. S3 posiada wsparcie dla protokołu HLS, więc bez problemu będziemy mogli wczytywać filmy bezpośrednio z S3 do naszego playera.
W ustawieniach bucketa justjoinit-video-output
(zakładka permissions), odznaczamy “Block all public access”, ponieważ nasze pliki będą dostępne publicznie.
I następnie edytujemy bucket policy, aby pliki, które są w nim umieszczone, były publicznie dostępne.
{ "Version": "2008-10-17", "Statement": [ { "Sid": "AllowPublicRead", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::justjoinit-video-output/*" } ] }
Ostatnią rzeczą jest ustawienie CORS, tak aby przeglądarka nie zablokowała zapytania.
[ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "PUT", "POST", "HEAD" ], "AllowedOrigins": [ "http://localhost" ], "ExposeHeaders": [] }, { "AllowedHeaders": [], "AllowedMethods": [ "GET" ], "AllowedOrigins": [ "*" ], "ExposeHeaders": [] } ]
Konfiguracja AWS Elastic Transcoder
Elastic Transcoder jest usługą do konwersji video – to rozwiązanie korzystne cenowo (jeśli nie konwertujemy bardzo dużej ilości filmów) oraz szybkie w obsłudze. W naszym przypadku użyjemy go do przetwarzania filmów na format M3U8.
Wartości .env:
- Pipeline ID –
AWS_PIPELINE_ID
Elastic Transcoder Preset
Użyjemy stworzony już Preset pod HLS “System preset: HLS 2M” ze zmienioną rozdzielczością oraz liczbą klatek na sekundę. W większości zastosowań, takie ustawienia powinny wystarczyć.
Możliwości ustawień są duże, więc każdy jest w stanie dostosować je do swoich wymagań.
Wartości .env:
- ID –
AWS_PRESET_ID
Konfiguracja Identity and Access Management (IAM)
IAM czyli Identity Access Management – służy do zarządzania dostępem do usług na koncie AWS, umożliwia kontrolę nad użytkownikami oraz ich uprawnieniami.
Tworzymy nowego użytkownika “app” z dostępem “programmatic access”.
Następnie wybieramy odpowiednie uprawnienia dla naszego użytkownika. W tym przypadku będzie to: AmazonS3FullAccess
oraz AmazonElasticTranscoder_JobsSubmitter
.
Sprawdzamy czy wszystko się zgadza i zatwierdzamy.
Wartości .env:
- Access key ID –
AWS_ACCESS_KEY_ID
- Secret access key –
AWS_SECRET_ACCESS_KEY
Platforma video – Laravel 8
Cały projekt aplikacji znajduje się w repozytorium na Githubie, więc zajmiemy się wyłącznie kluczowymi fragmentami projektu. Podczas pisania kodu skupiłem się wyłącznie na samej funkcjonalności video, więc nie ma tam m.in. logowania/rejestracji, dokładnej walidacji czy złożonego designu.
Composer
Zaczniemy od instalacji potrzebnych bibliotek:
composer require league/flysystem-aws-s3-v3 composer require aws/aws-sdk-php
Konfiguracja zmiennych
Zanim zaczniemy pracę z AWS SDK, musimy ustawić zmienne (klucze dostępu).
.env
AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=eu-west-1 AWS_BUCKET=justjoinit-video-input AWS_URL=https://justjoinit-video-output.s3-eu-west-1.amazonaws.com AWS_PIPELINE_ID= AWS_PRESET_ID=
Link do S3 jesteśmy w stanie wygenerować sami według wzoru:
https://{BUCKET_NAME}.s3-{REGION}.amazonaws.com
config/services.php
<?php return [ //.... 'ses' => [ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], 'transcoder' => [ 'pipeline_id' => env('AWS_PIPELINE_ID'), 'preset_id' => env('AWS_PRESET_ID'), 'url' => env('AWS_URL'), ], ];
Model oraz migracja
php artisan make:model Video --migration
database/migrations/create_videos_table.php
<?php use IlluminateDatabaseMigrationsMigration; use IlluminateDatabaseSchemaBlueprint; use IlluminateSupportFacadesSchema; class CreateVideosTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('videos', function (Blueprint $table) { $table->string('id')->primary(); $table->string('title'); $table->string('extension'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('videos'); } }
Jako primary key użyłem losowego ciągu znaków, ponieważ auto increment jest zbyt łatwy do odgadnięcia, więc ewentualne filmy niepubliczne (dostęp wyłącznie przez link) nie miałyby sensu.
Controller
W kontrolerze walidujemy request używając Form Requests, tworzymy model oraz zapisujemy wgrany plik.
app/Http/Controllers/VideoController.php
<?php namespace AppHttpControllers; use AppModelsVideo; use IlluminateSupportStr; use AppJobsProcessVideoJob; use AppHttpRequestsStoreVideo; class VideoController extends Controller { //... public function store(StoreVideo $request) { $id = Str::random(5); $file = $request->file('video'); $extension = $file->getClientOriginalExtension(); $video = Video::create([ 'id' => $id, 'title' => $request->input('title'), 'extension' => $extension, ]); $file->storeAs('tmp-video', $id . '.' . $extension); ProcessVideoJob::dispatch($video); return redirect()->route('index'); } //... }
Job
Natychmiast po wgraniu filmu, ProcessVideoJob
wysyłany jest do kolejki. Po uruchomieniu następuje przesłanie pliku video na S3.
<?php namespace AppJobs; use AppModelsVideo; use AwsElasticTranscoderElasticTranscoderClient; use IlluminateSupportFacadesStorage; use IlluminateBusQueueable; use IlluminateContractsQueueShouldBeUnique; use IlluminateContractsQueueShouldQueue; use IlluminateFoundationBusDispatchable; use IlluminateQueueInteractsWithQueue; use IlluminateQueueSerializesModels; class ProcessVideoJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; /** * @var AppModelsVideo */ protected $video; /** * Create a new job instance. * * @return void */ public function __construct(Video $video) { $this->video = $video; } /** * Execute the job. * * @return void */ public function handle() { $id = $this->video->id; $path = 'tmp-video/' . $id . '.' . $this->video->extension; $s3Path = $id . '.' . $this->video->extension; Storage::disk('s3')->writeStream($s3Path, Storage::disk('local')->readStream($path)); $client = new ElasticTranscoderClient([ 'credentials' => [ 'key' => config('services.ses.key'), 'secret' => config('services.ses.secret'), ], 'region' => config('services.ses.region'), 'version' => '2012-09-25' ]); $client->createJob([ 'PipelineId' => config('services.transcoder.pipeline_id'), 'Input' => [ 'Key' => $s3Path, ], 'Output' => [ 'Key' => $id, 'SegmentDuration' => '10', 'PresetId' => config('services.transcoder.preset_id'), 'ThumbnailPattern' => $id . '-{count}', ], ]); } }
Kluczowym fragmentem jest sam sposób przesyłania.
Metody write
oraz read
wczytują plik do pamięci co może być problematyczne w przypadku dużych plików, ogranicza nas maksymalny rozmiar zmiennej.
Z tego powodu użyłem writeStream
i readStream
, które używają strumienia do przesyłu danych.
Helper
Do generowania linków do S3 przyda się prosty helper.
app/helpers.php
<?php if (! function_exists('s3_url')) { /** * Generate asset link for S3 * * @param string $file * @return string */ function s3_url($file) { $baseUrl = config('services.transcoder.url'); return $baseUrl . '/' . $file; } }
Player
Jako player użyłem video.js, który według mnie jest najlepszą opcją dla tego typu stron. Dla ułatwienia zakładamy, że każdy zasób na S3 jest dostępny (przetworzony), więc będziemy w “ciemno” wyświetlać linki na widokach. W celu otrzymywania powiadomień odnośnie statusu przetwarzania można skorzystać z usługi Amazon Simple Notification.
<video id="video-player" class="video-js vjs-16-9" controls preload="auto" poster="{{ s3_url($video->id . '-00001.png') }}" data-setup="{}"> <source src="{{ s3_url($video->id . '.m3u8') }}" type="application/x-mpegURL" /> </video>
Platforma video z AWS Elastic Transcoder i AWS S3 gotowa!
Według mnie jest to najlepsze rozwiązanie dla osób, które chcą stworzyć skalowalną oraz wydajną platformę video. Oczywiście skonfigurowanie wszystkich usług AWS może być problematyczne dla osób niewtajemniczonych w rozwiązania firmy Amazon, lecz praktyka czyni mistrza. Zachęcam wszystkich zainteresowanych do rozwoju mojego kodu we własnym zakresie.
Zdjęcie główne artykułu pochodzi z unsplash.com.