Laravel Shaka is built as a set of clean, testable layers, following the same pattern used by PHP-FFmpeg and Laravel FFmpeg.
ShakaPackager)This layer talks directly to the Shaka Packager binary:
namespace Foxws\Shaka\Support\Packager;
class ShakaPackager
{
protected string $binaryPath;
protected ?LoggerInterface $logger;
protected int $timeout;
// Binary execution
public function command(string $command): string;
// Version detection
public function getVersion(): string;
// Configuration
public function setTimeout(int $timeout): self;
}
What it's responsible for:
Process facadeWhy it's split out:
Packager)This layer is the high-level API you actually call:
namespace Foxws\Shaka\Support\Packager;
class Packager
{
protected ShakaPackager $driver;
protected ?MediaCollection $mediaCollection;
protected ?CommandBuilder $builder;
// Media management
public function open(MediaCollection $mediaCollection): self;
// Stream configuration
public function addVideoStream(string $input, string $output, array $options = []): self;
public function addAudioStream(string $input, string $output, array $options = []): self;
// Output configuration
public function withMpdOutput(string $path): self;
public function withHlsMasterPlaylist(string $path): self;
// Execution
public function export(): PackagerResult;
}
What it's responsible for:
CommandBuilderWhy it's split out:
Shaka & MediaOpenerFactory)This layer provides the Laravel-style interface you interact with day to day:
namespace Foxws\Shaka;
class Shaka
{
protected ?Disk $disk;
protected ?Packager $packager;
protected ?MediaCollection $collection;
// Disk management
public function fromDisk(Filesystem|string $disk): self;
public function openFromDisk(Filesystem|string $disk, $paths): self;
// Media management
public function open($paths): self;
// Forwards all Packager methods
public function __call($method, $arguments);
}
What it's responsible for:
PackagerWhy it's split out:
┌─────────────────────────────────────────────┐
│ Shaka (Facade) │
│ - Disk management │
│ - Media file opening │
│ - Method forwarding │
└────────────────┬────────────────────────────┘
│
├──> MediaCollection (Media files)
│
v
┌─────────────────────────────────────────────┐
│ Packager (Business Logic) │
│ - Stream configuration │
│ - Command building │
│ - Fluent API │
└────────────────┬────────────────────────────┘
│
├──> CommandBuilder (Command construction)
├──> Stream (Stream objects)
│
v
┌─────────────────────────────────────────────┐
│ ShakaPackager (Binary) │
│ - Binary execution │
│ - Process management │
│ - Error handling │
└────────────────┬────────────────────────────┘
│
v
[Shaka Packager Binary]
Builds packager command strings, fluently:
$builder = CommandBuilder::make()
->addVideoStream('input.mp4', 'video.mp4')
->addAudioStream('input.mp4', 'audio.mp4')
->withMpdOutput('manifest.mpd')
->withSegmentDuration(6);
$command = $builder->build();
Represents a single, immutable stream configuration. setOutput(), setOptions(), and addOption() each return a new instance rather than changing the current one:
$stream = Stream::video($media)
->setOutput('video.mp4')
->addOption('bandwidth', '5000000');
$commandString = $stream->toCommandString();
// "in=/path/to/input.mp4,stream=video,output=video.mp4,bandwidth=5000000"
The structured result returned from a packaging operation:
$result = $packager->export();
$output = $result->getOutput();
$result->toDisk('s3'); // Copy the temp output to a target disk
$result->hasCopyFailures();
$result->getFailedFiles(); // array<int, CopyFailure>
$result->getEncryptionKeys(); // array<int, EncryptionKeyFile>
Represents your input media files:
$media = Media::make($disk, 'video.mp4');
$collection = MediaCollection::make([$media]);
$localPath = $media->getLocalPath();
$filename = $media->getFilename();
The package uses Laravel's service container for dependency injection:
// ShakaServiceProvider.php
// Register driver
$this->app->singleton(ShakaPackager::class, function ($app) {
$logger = $app->make('laravel-shaka-logger');
$config = $app->make('laravel-shaka-configuration');
return ShakaPackager::create($logger, $config);
});
// Register packager (scoped, not singleton: it holds per-export state like
// the CommandBuilder and temp directory, which must not leak across requests
// under Octane)
$this->app->scoped(Packager::class, function ($app) {
$driver = $app->make(ShakaPackager::class);
$logger = $app->make('laravel-shaka-logger');
return new Packager($driver, $logger);
});
The package uses a clear exception hierarchy. One thing worth knowing: ExecutableNotFoundException exists, but nothing currently throws it. A missing or non-executable binary instead surfaces as a RuntimeException from the underlying Process call, the first time the packager binary is actually invoked:
try {
$result = Shaka::open('input.mp4')->export();
} catch (RuntimeException $e) {
// Command execution failed (including: binary not found/not executable)
} catch (InvalidArgumentException $e) {
// Invalid input
}
This layered design makes testing straightforward:
// Mock the driver
$driver = Mockery::mock(ShakaPackager::class);
$driver->shouldReceive('command')->andReturn('success');
$packager = new Packager($driver);
$result = $packager->open($collection)->export();
Extend the driver for custom behavior:
class CustomPackagerDriver extends ShakaPackager
{
public function customOperation(array $options): string
{
$command = $this->buildCustomCommand($options);
return $this->command($command);
}
}
Stream's constructor is protected, not private, specifically so it can be subclassed. Its mutator methods use new static(...) so a subclass instance survives with*() calls. Give your subclass its own named constructor rather than overriding make() — its signature (Media $media, string $type = 'video') won't accept an incompatible override:
class SubtitleStream extends Stream
{
public static function subtitle(Media $media): self
{
return new self($media, null, 'text');
}
}
Extend the result objects:
class DetailedPackagerResult extends PackagerResult
{
public function getKeyCount(): int
{
return count($this->getEncryptionKeys());
}
}
Packager from the container rather than constructing it yourself.Shaka::open() covers most day-to-day tasks.php artisan shaka:info.cleanupTemporaryFiles().withAESEncryption() for DRM content, see AES Encryption.