From 7b088af60bb874fce4aad71e0e8bf4b762497d45 Mon Sep 17 00:00:00 2001 From: Jaydeep Rokade Date: Sat, 21 Jun 2025 10:28:43 +0530 Subject: [PATCH] Refactor Disk, MediaExporter, HLSExporter, and MediaOpener for PHP 7.4+ compatibility and code clarity --- src/Exporters/HLSExporter.php | 162 ++++++-------------------------- src/Exporters/MediaExporter.php | 77 ++++----------- src/Filesystem/Disk.php | 41 +++++++- src/MediaOpener.php | 107 +++------------------ 4 files changed, 96 insertions(+), 291 deletions(-) diff --git a/src/Exporters/HLSExporter.php b/src/Exporters/HLSExporter.php index 49c3dd8..65a823e 100755 --- a/src/Exporters/HLSExporter.php +++ b/src/Exporters/HLSExporter.php @@ -17,62 +17,29 @@ class HLSExporter extends MediaExporter use EncryptsHLSSegments; public const HLS_KEY_INFO_FILENAME = 'hls_encryption.keyinfo'; - public const ENCRYPTION_LISTENER = 'listen-encryption-key'; - /** - * @var int - */ - private $segmentLength = 10; - - /** - * @var int - */ - private $keyFrameInterval = 48; - - /** - * @var \Illuminate\Support\Collection - */ - private $pendingFormats; - - /** - * @var \ProtoneMedia\LaravelFFMpeg\Exporters\PlaylistGenerator - */ - private $playlistGenerator; - - /** - * @var \Closure - */ - private $segmentFilenameGenerator = null; - - /** - * Setter for the segment length - */ + private int $segmentLength = 10; + private int $keyFrameInterval = 48; + private Collection $pendingFormats; + private ?PlaylistGenerator $playlistGenerator = null; + private ?Closure $segmentFilenameGenerator = null; + public function setSegmentLength(int $length): self { $this->segmentLength = max(2, $length); - return $this; } - /** - * Setter for the Key Frame interval - */ public function setKeyFrameInterval(int $interval): self { $this->keyFrameInterval = max(2, $interval); - return $this; } - /** - * Method to set a different playlist generator than - * the default HLSPlaylistGenerator. - */ public function withPlaylistGenerator(PlaylistGenerator $playlistGenerator): self { $this->playlistGenerator = $playlistGenerator; - return $this; } @@ -81,9 +48,6 @@ private function getPlaylistGenerator(): PlaylistGenerator return $this->playlistGenerator ??= new HLSPlaylistGenerator; } - /** - * Method to not add the #EXT-X-ENDLIST line to the playlist. - */ public function withoutPlaylistEndLine(): self { $playlistGenerator = $this->getPlaylistGenerator(); @@ -95,22 +59,15 @@ public function withoutPlaylistEndLine(): self return $this; } - /** - * Setter for a callback that generates a segment filename. - */ public function useSegmentFilenameGenerator(Closure $callback): self { $this->segmentFilenameGenerator = $callback; - return $this; } - /** - * Returns a default generator if none is set. - */ private function getSegmentFilenameGenerator(): callable { - return $this->segmentFilenameGenerator ?: function ($name, $format, $key, $segments, $playlist) { + return $this->segmentFilenameGenerator ?? function ($name, $format, $key, $segments, $playlist) { $bitrate = $this->driver->getVideoStream() ? $format->getKiloBitrate() : $format->getAudioKiloBitrate(); @@ -120,9 +77,6 @@ private function getSegmentFilenameGenerator(): callable }; } - /** - * Calls the generator with the path (without extension), format and key. - */ private function getSegmentPatternAndFormatPlaylistPath(string $baseName, AudioInterface $format, int $key): array { $segmentsPattern = null; @@ -144,28 +98,17 @@ function ($path) use (&$formatPlaylistPath) { return [$segmentsPattern, $formatPlaylistPath]; } - /** - * Merges the HLS parameters to the given format. - * - * @param \FFMpeg\Format\Video\DefaultAudio $format - */ private function addHLSParametersToFormat(DefaultAudio $format, string $segmentsPattern, Disk $disk, int $key): array { $format->setAdditionalParameters(array_merge( $format->getAdditionalParameters() ?: [], $hlsParameters = [ - '-sc_threshold', - '0', - '-g', - $this->keyFrameInterval, - '-hls_playlist_type', - 'vod', - '-hls_time', - $this->segmentLength, - '-hls_segment_filename', - $disk->makeMedia($segmentsPattern)->getLocalPath(), - '-master_pl_name', - $this->generateTemporarySegmentPlaylistFilename($key), + '-sc_threshold', '0', + '-g', $this->keyFrameInterval, + '-hls_playlist_type', 'vod', + '-hls_time', $this->segmentLength, + '-hls_segment_filename', $disk->makeMedia($segmentsPattern)->getLocalPath(), + '-master_pl_name', self::generateTemporarySegmentPlaylistFilename($key), ], $this->getEncrypedHLSParameters() )); @@ -173,11 +116,6 @@ private function addHLSParametersToFormat(DefaultAudio $format, string $segments return $hlsParameters; } - /** - * Gives the callback an HLSVideoFilters object that provides addFilter(), - * addLegacyFilter(), addWatermark() and resize() helper methods. It - * returns a mapping for the video and (optional) audio stream. - */ private function applyFiltersCallback(callable $filtersCallback, int $formatKey): array { $filtersCallback( @@ -195,101 +133,64 @@ private function applyFiltersCallback(callable $filtersCallback, int $formatKey) return $outs; } - /** - * Returns the filename of a segment playlist by its key. We let FFmpeg generate a playlist - * for each added format so we don't have to detect the bitrate and codec ourselves. - * We use this as a reference so when can generate our own main playlist. - */ public static function generateTemporarySegmentPlaylistFilename(int $key): string { return "temporary_segment_playlist_{$key}.m3u8"; } - /** - * Loops through each added format and then deletes the temporary - * segment playlist, which we generate manually using the - * HLSPlaylistGenerator. - */ private function cleanupSegmentPlaylistGuides(Media $media): self { $disk = $media->getDisk(); $directory = $media->getDirectory(); - $this->pendingFormats->map(function ($formatAndCallback, $key) use ($disk, $directory) { - $disk->delete($directory.static::generateTemporarySegmentPlaylistFilename($key)); + $this->pendingFormats->each(function ($formatAndCallback, $key) use ($disk, $directory) { + $disk->delete($directory . self::generateTemporarySegmentPlaylistFilename($key)); }); return $this; } - /** - * Adds a mapping for each added format and automatically handles the mapping - * for filters. Adds a handler to rotate the encryption key (optional). - * Returns a media collection of all segment playlists. - * - * @throws \ProtoneMedia\LaravelFFMpeg\Exporters\NoFormatException - */ private function prepareSaving(?string $path = null): Collection { - if (! $this->pendingFormats) { + if (!$this->pendingFormats) { throw new NoFormatException; } $media = $this->getDisk()->makeMedia($path); - - $baseName = $media->getDirectory().$media->getFilenameWithoutExtension(); + $baseName = $media->getDirectory() . $media->getFilenameWithoutExtension(); return $this->pendingFormats->map(function (array $formatAndCallback, $key) use ($baseName) { [$format, $filtersCallback] = $formatAndCallback; [$segmentsPattern, $formatPlaylistPath] = $this->getSegmentPatternAndFormatPlaylistPath( - $baseName, - $format, - $key + $baseName, $format, $key ); - $disk = $this->getDisk()->clone(); - + $disk = $this->getDisk()->cloneDisk(); $this->addHLSParametersToFormat($format, $segmentsPattern, $disk, $key); - if ($filtersCallback) { - $outs = $this->applyFiltersCallback($filtersCallback, $key); - } + $outs = $filtersCallback ? $this->applyFiltersCallback($filtersCallback, $key) : ['0']; + $formatPlaylistOutput = $disk->makeMedia($formatPlaylistPath); - $this->addFormatOutputMapping($format, $formatPlaylistOutput, $outs ?? ['0']); + $this->addFormatOutputMapping($format, $formatPlaylistOutput, $outs); return $formatPlaylistOutput; - })->tap(function () { - $this->addHandlerToRotateEncryptionKey(); - }); + })->tap(fn() => $this->addHandlerToRotateEncryptionKey()); } - /** - * Prepares the saves command but returns the command instead. - * - * @return mixed - */ public function getCommand(?string $path = null) { $this->prepareSaving($path); - return parent::getCommand(null); } - /** - * Runs the export, generates the main playlist, and cleans up the - * segment playlist guides and temporary HLS encryption keys. - * - * @param string $path - */ public function save(?string $mainPlaylistPath = null): MediaOpener { return $this->prepareSaving($mainPlaylistPath)->pipe(function ($segmentPlaylists) use ($mainPlaylistPath) { $result = parent::save(); $playlist = $this->getPlaylistGenerator()->get( - $segmentPlaylists->all(), - $this->driver->fresh() + $segmentPlaylists->all(), $this->driver->fresh() ); $this->getDisk()->put($mainPlaylistPath, $playlist); @@ -303,21 +204,14 @@ public function save(?string $mainPlaylistPath = null): MediaOpener }); } - /** - * Initializes the $pendingFormats property when needed and adds the format - * with the optional callback to add filters. - */ public function addFormat(FormatInterface $format, ?callable $filtersCallback = null): self { - if (! $this->pendingFormats) { - $this->pendingFormats = new Collection; - } + $this->pendingFormats ??= new Collection; - if (! $format instanceof DefaultVideo && $format instanceof DefaultAudio) { + if (!$format instanceof DefaultVideo && $format instanceof DefaultAudio) { $originalFormat = clone $format; - $format = new class extends DefaultVideo - { + $format = new class extends DefaultVideo { private array $audioCodecs = []; public function setAvailableAudioCodecs(array $audioCodecs) @@ -335,7 +229,7 @@ public function supportBFrames() return false; } - public function getAvailableVideoCodecs() + public function getAvailableVideoCodecs(): array { return []; } diff --git a/src/Exporters/MediaExporter.php b/src/Exporters/MediaExporter.php index fdc536f..75bc4c8 100755 --- a/src/Exporters/MediaExporter.php +++ b/src/Exporters/MediaExporter.php @@ -27,38 +27,21 @@ class MediaExporter use HandlesTimelapse; use HasProgressListener; - /** - * @var \ProtoneMedia\LaravelFFMpeg\Drivers\PHPFFMpeg - */ - protected $driver; - - /** - * @var \FFMpeg\Format\FormatInterface - */ - private $format; - - /** - * @var string - */ - protected $visibility; + protected PHPFFMpeg $driver; + private ?FormatInterface $format = null; + protected ?string $visibility = null; + private ?Disk $toDisk = null; /** - * @var \ProtoneMedia\LaravelFFMpeg\Filesystem\Disk - */ - private $toDisk; - - /** - * Callbacks that should be called directly after the - * underlying library completed the save method. + * Callbacks to execute after saving. * - * @var array + * @var array */ - private $afterSavingCallbacks = []; + private array $afterSavingCallbacks = []; public function __construct(PHPFFMpeg $driver) { $this->driver = $driver; - $this->maps = new Collection; } @@ -68,38 +51,32 @@ protected function getDisk(): Disk return $this->toDisk; } - $media = $this->driver->getMediaCollection(); - /** @var Disk $disk */ - $disk = $media->first()->getDisk(); + $disk = $this->driver->getMediaCollection()->first()->getDisk(); - return $this->toDisk = $disk->clone(); + return $this->toDisk = $disk->cloneDisk(); // updated to match Disk.php changes } public function inFormat(FormatInterface $format): self { $this->format = $format; - return $this; } - public function toDisk($disk) + public function toDisk($disk): self { $this->toDisk = Disk::make($disk); - return $this; } - public function withVisibility(string $visibility) + public function withVisibility(string $visibility): self { $this->visibility = $visibility; - return $this; } /** - * Calls the callable with a TileFactory instance and - * adds the freshly generated TileFilter. + * Add tile filter and generate VTT thumbnails. */ public function addTileFilter(callable $withTileFactory): self { @@ -109,7 +86,7 @@ public function addTileFilter(callable $withTileFactory): self $this->addFilter($filter = $tileFactory->get()); - if (! $tileFactory->vttOutputPath) { + if (!$tileFactory->vttOutputPath) { return $this; } @@ -117,7 +94,7 @@ public function addTileFilter(callable $withTileFactory): self $generator = new VTTPreviewThumbnailsGenerator( $filter, $mediaExporter->driver->getDurationInSeconds(), - $tileFactory->vttSequnceFilename ?: fn () => $outputMedia->getPath() + $tileFactory->vttSequenceFilename ?: fn () => $outputMedia->getPath() // fixed typo ); $this->toDisk->put($tileFactory->vttOutputPath, $generator->getContents()); @@ -125,9 +102,7 @@ public function addTileFilter(callable $withTileFactory): self } /** - * Returns the final command, useful for debugging purposes. - * - * @return mixed + * Get the final FFMpeg command string. */ public function getCommand(?string $path = null) { @@ -139,23 +114,14 @@ public function getCommand(?string $path = null) ); } - /** - * Dump the final command and end the script. - * - * @return void - */ - public function dd(?string $path = null) + public function dd(?string $path = null): void { dd($this->getCommand($path)); } - /** - * Adds a callable to the callbacks array. - */ public function afterSaving(callable $callback): self { $this->afterSavingCallbacks[] = $callback; - return $this; } @@ -169,9 +135,7 @@ private function prepareSaving(?string $path = null): ?Media if ($this->maps->isNotEmpty()) { $this->driver->getPendingComplexFilters()->each->apply($this->driver, $this->maps); - $this->maps->map->apply($this->driver->get()); - return $outputMedia; } @@ -186,11 +150,10 @@ private function prepareSaving(?string $path = null): ?Media return $outputMedia; } - protected function runAfterSavingCallbacks(?Media $outputMedia = null) + protected function runAfterSavingCallbacks(?Media $outputMedia = null): void { foreach ($this->afterSavingCallbacks as $key => $callback) { call_user_func($callback, $this, $outputMedia); - unset($this->afterSavingCallbacks[$key]); } } @@ -217,7 +180,6 @@ public function save(?string $path = null) if ($this->returnFrameContents) { $this->runAfterSavingCallbacks($outputMedia); - return $data; } } else { @@ -283,14 +245,9 @@ protected function getMediaOpener(): MediaOpener ); } - /** - * Forwards the call to the driver object and returns the result - * if it's something different than the driver object itself. - */ public function __call($method, $arguments) { $result = $this->forwardCallTo($driver = $this->driver, $method, $arguments); - return ($result === $driver) ? $this : $result; } } diff --git a/src/Filesystem/Disk.php b/src/Filesystem/Disk.php index adc99ef..172e62b 100755 --- a/src/Filesystem/Disk.php +++ b/src/Filesystem/Disk.php @@ -23,12 +23,12 @@ class Disk private $disk; /** - * @var string + * @var string|null */ private $temporaryDirectory; /** - * @var \Illuminate\Filesystem\FilesystemAdapter + * @var \Illuminate\Filesystem\FilesystemAdapter|null */ private $filesystemAdapter; @@ -61,7 +61,7 @@ public static function makeTemporaryDisk(): self /** * Creates a fresh instance, mostly used to force a new TemporaryDirectory. */ - public function clone(): self + public function cloneDisk(): self { return new Disk($this->disk); } @@ -79,6 +79,9 @@ public function getTemporaryDirectory(): string return $this->temporaryDirectory = app(TemporaryDirectories::class)->create(); } + /** + * Creates a Media instance for the given path. + */ public function makeMedia(string $path): Media { return Media::make($this, $path); @@ -94,9 +97,12 @@ public function getName(): string return $this->disk; } - return get_class($this->getFlysystemAdapter()).'_'.md5(spl_object_id($this->getFlysystemAdapter())); + return get_class($this->getFlysystemAdapter()) . '_' . md5(spl_object_id($this->getFlysystemAdapter())); } + /** + * Returns the Laravel FilesystemAdapter, initializing if not already set. + */ public function getFilesystemAdapter(): FilesystemAdapter { if ($this->filesystemAdapter) { @@ -110,16 +116,25 @@ public function getFilesystemAdapter(): FilesystemAdapter return $this->filesystemAdapter = Storage::disk($this->disk); } + /** + * Returns the underlying Flysystem driver. + */ private function getFlysystemDriver(): LeagueFilesystem { return $this->getFilesystemAdapter()->getDriver(); } + /** + * Returns the Flysystem adapter (e.g., Local, S3). + */ private function getFlysystemAdapter(): FlysystemFilesystemAdapter { return $this->getFilesystemAdapter()->getAdapter(); } + /** + * Returns true if the disk is using a LocalFilesystemAdapter. + */ public function isLocalDisk(): bool { return $this->getFlysystemAdapter() instanceof LocalFilesystemAdapter; @@ -143,9 +158,25 @@ public function path(string $path): string return $this->isLocalDisk() ? static::normalizePath($path) : $path; } + /** + * Check if the file exists on the disk. + */ + public function fileExists(string $path): bool + { + return $this->getFilesystemAdapter()->exists($path); + } + + /** + * Deletes the file if it exists. + */ + public function deleteIfExists(string $path): bool + { + return $this->fileExists($path) ? $this->getFilesystemAdapter()->delete($path) : false; + } + /** * Forwards all calls to Laravel's FilesystemAdapter which will pass - * dynamic methods call onto Flysystem. + * dynamic method calls onto Flysystem. */ public function __call($method, $parameters) { diff --git a/src/MediaOpener.php b/src/MediaOpener.php index ed115b7..2759807 100755 --- a/src/MediaOpener.php +++ b/src/MediaOpener.php @@ -28,62 +28,29 @@ class MediaOpener { use ForwardsCalls; - /** - * @var \ProtoneMedia\LaravelFFMpeg\Filesystem\Disk - */ - private $disk; - - /** - * @var \ProtoneMedia\LaravelFFMpeg\Drivers\PHPFFMpeg - */ - private $driver; - - /** - * @var \ProtoneMedia\LaravelFFMpeg\Filesystem\MediaCollection - */ - private $collection; - - /** - * @var \FFMpeg\Coordinate\TimeCode - */ - private $timecode; - - /** - * Uses the 'filesystems.default' disk from the config if none is given. - * Gets the underlying PHPFFMpeg instance from the container if none is given. - * Instantiates a fresh MediaCollection if none is given. - */ + private Disk $disk; + private PHPFFMpeg $driver; + private MediaCollection $collection; + private ?TimeCode $timecode = null; + public function __construct($disk = null, ?PHPFFMpeg $driver = null, ?MediaCollection $mediaCollection = null) { $this->fromDisk($disk ?: config('filesystems.default')); - $this->driver = ($driver ?: app(PHPFFMpeg::class))->fresh(); - $this->collection = $mediaCollection ?: new MediaCollection; } - public function clone(): self + public function cloneOpener(): self { - return new MediaOpener( - $this->disk, - $this->driver, - $this->collection - ); + return new self($this->disk, $this->driver, $this->collection); } - /** - * Set the disk to open files from. - */ public function fromDisk($disk): self { $this->disk = Disk::make($disk); - return $this; } - /** - * Alias for 'fromDisk', mostly for backwards compatibility. - */ public function fromFilesystem(Filesystem $filesystem): self { return $this->fromDisk($filesystem); @@ -91,22 +58,15 @@ public function fromFilesystem(Filesystem $filesystem): self private static function makeLocalDiskFromPath(string $path): Disk { - $adapter = (new FilesystemManager(app()))->createLocalDriver([ - 'root' => $path, - ]); - + $adapter = (new FilesystemManager(app()))->createLocalDriver(['root' => $path]); return Disk::make($adapter); } - /** - * Instantiates a Media object for each given path. - */ public function open($paths): self { foreach (Arr::wrap($paths) as $path) { if ($path instanceof UploadedFile) { - $disk = static::makeLocalDiskFromPath($path->getPath()); - + $disk = self::makeLocalDiskFromPath($path->getPath()); $media = Media::make($disk, $path->getFilename()); } else { $media = Media::make($this->disk, $path); @@ -118,9 +78,6 @@ public function open($paths): self return $this; } - /** - * Instantiates a single Media object and sets the given options on the object. - */ public function openWithInputOptions(string $path, array $options = []): self { $this->collection->push( @@ -130,9 +87,6 @@ public function openWithInputOptions(string $path, array $options = []): self return $this; } - /** - * Instantiates a MediaOnNetwork object for each given url. - */ public function openUrl($paths, array $headers = []): self { foreach (Arr::wrap($paths) as $path) { @@ -152,25 +106,16 @@ public function getDriver(): PHPFFMpeg return $this->driver->open($this->collection); } - /** - * Forces the driver to open the collection with the `openAdvanced` method. - */ public function getAdvancedDriver(): PHPFFMpeg { return $this->driver->openAdvanced($this->collection); } - /** - * Shortcut to set the timecode by string. - */ public function getFrameFromString(string $timecode): self { return $this->getFrameFromTimecode(TimeCode::fromString($timecode)); } - /** - * Shortcut to set the timecode by seconds. - */ public function getFrameFromSeconds(float $seconds): self { return $this->getFrameFromTimecode(TimeCode::fromSeconds($seconds)); @@ -179,13 +124,9 @@ public function getFrameFromSeconds(float $seconds): self public function getFrameFromTimecode(TimeCode $timecode): self { $this->timecode = $timecode; - return $this; } - /** - * Returns an instance of MediaExporter with the driver and timecode (if set). - */ public function export(): MediaExporter { return tap(new MediaExporter($this->getDriver()), function (MediaExporter $mediaExporter) { @@ -195,17 +136,11 @@ public function export(): MediaExporter }); } - /** - * Returns an instance of HLSExporter with the driver forced to AdvancedMedia. - */ public function exportForHLS(): HLSExporter { return new HLSExporter($this->getAdvancedDriver()); } - /** - * Returns an instance of MediaExporter with a TileFilter and ImageFormat. - */ public function exportTile(callable $withTileFactory): MediaExporter { return $this->export() @@ -216,54 +151,42 @@ public function exportTile(callable $withTileFactory): MediaExporter public function exportFramesByAmount(int $amount, ?int $width = null, ?int $height = null, ?int $quality = null): MediaExporter { $interval = ($this->getDurationInSeconds() + 1) / $amount; - return $this->exportFramesByInterval($interval, $width, $height, $quality); } public function exportFramesByInterval(float $interval, ?int $width = null, ?int $height = null, ?int $quality = null): MediaExporter { - return $this->exportTile( - fn (TileFactory $tileFactory) => $tileFactory - ->interval($interval) - ->grid(1, 1) - ->scale($width, $height) - ->quality($quality) + return $this->exportTile(fn (TileFactory $tileFactory) => + $tileFactory->interval($interval) + ->grid(1, 1) + ->scale($width, $height) + ->quality($quality) ); } public function cleanupTemporaryFiles(): self { app(TemporaryDirectories::class)->deleteAll(); - return $this; } public function each($items, callable $callback): self { Collection::make($items)->each(function ($item, $key) use ($callback) { - return $callback($this->clone(), $item, $key); + return $callback($this->cloneOpener(), $item, $key); }); return $this; } - /** - * Returns the Media object from the driver. - */ public function __invoke(): AbstractMediaType { return $this->getDriver()->get(); } - /** - * Forwards all calls to the underlying driver. - * - * @return void - */ public function __call($method, $arguments) { $result = $this->forwardCallTo($driver = $this->getDriver(), $method, $arguments); - return ($result === $driver) ? $this : $result; } }