Skip to content

Commit

Permalink
Encrypted HLS (#262)
Browse files Browse the repository at this point in the history
* Dropped support for PHP 7.2
* Update EncryptsHLSSegments.php
* Update README.md
* Update HlsExportTest.php
* Refactor + docs
* Update CHANGELOG.md
* Update composer.json
* Update run-tests.yml
* Support for dynamic playlists

Co-authored-by: Pascal Baljet <[email protected]>
  • Loading branch information
pascalbaljet and pascalbaljet authored Dec 22, 2020
1 parent 165715c commit 8df75e9
Show file tree
Hide file tree
Showing 27 changed files with 1,302 additions and 108 deletions.
11 changes: 1 addition & 10 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [7.4, 7.3, 7.2]
php: [8.0, 7.4, 7.3]
laravel: [8.*, 7.*, 6.*]
dependency-version: [prefer-lowest, prefer-stable]
include:
Expand All @@ -18,22 +18,13 @@ jobs:
testbench: 5.*
- laravel: 6.*
testbench: 4.*
exclude:
- laravel: 8.*
php: 7.2

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.composer/cache/files
key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
Expand Down
63 changes: 62 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,68 @@

All Notable changes to `pbmedia/laravel-ffmpeg` will be documented in this file

## 7.3.0 - 2020-10-??
## 7.5.0 - 2020-12-22

### Added

- Support for PHP 8.0.
- Encrypted HLS.
- New `getProcessOutput` method to analyze media.
- Support for dynamic HLS playlists.

### Deprecated

- Nothing

### Fixed

- Nothing

### Removed

- Support for PHP 7.2

## 7.4.1 - 2020-10-26

### Added

- Better exceptions
- dd() improvements

### Deprecated

- Nothing

### Fixed

- Nothing

### Removed

- Nothing

## 7.4.0 - 2020-10-25

### Added

- Watermark manipulations
- Dump and die
- Resize filter shortcut
- HLS export with multiple filters per format

### Deprecated

- Nothing

### Fixed

- Nothing

### Removed

- Nothing

## 7.3.0 - 2020-10-16

### Added

Expand Down
105 changes: 104 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ This package provides an integration with FFmpeg for Laravel 6.0 and higher. [La
* Integration with [Laravel's Filesystem](http://laravel.com/docs/7.0/filesystem), [configuration system](https://laravel.com/docs/7.0/configuration) and [logging handling](https://laravel.com/docs/7.0/errors).
* Compatible with Laravel 6.0 and higher, support for [Package Discovery](https://laravel.com/docs/7.0/packages#package-discovery).
* Built-in support for HLS.
* Built-in support for encrypted HLS (AES-128) and rotating keys (optional).
* Built-in support for concatenation, multiple inputs/outputs, image sequences (timelapse), complex filters (and mapping), frame/thumbnail exports.
* Built-in support for watermarks (positioning and manipulation).
* PHP 7.2 and higher.
* PHP 7.3 and higher.

## Support

Expand Down Expand Up @@ -589,6 +590,108 @@ FFMpeg::fromDisk('videos')
});
```

### Encrypted HLS

As of version 7.5, you can encrypt each HLS segment using AES-128 encryption. To do this, call the `withEncryptionKey` method on the HLS exporter with a key. We provide a `generateEncryptionKey` helper method on the `HLSExporter` class to generate a key. Make sure you store the key well, as the exported result is worthless without the key.

```php
use ProtoneMedia\LaravelFFMpeg\Exporters\HLSExporter;

$encryptionKey = HLSExporter::generateEncryptionKey();

FFMpeg::open('steve_howe.mp4')
->exportForHLS()
->withEncryptionKey($encryptionKey)
->addFormat($lowBitrate)
->addFormat($midBitrate)
->addFormat($highBitrate)
->save('adaptive_steve.m3u8');
```

To secure your HLS export even further, you can rotate the key on each exported segment. By doing so, it will generate multiple keys that you'll need to store. Use the `withRotatingEncryptionKey` method to enable this feature and provide a callback that implements the storage of the keys.

```php
FFMpeg::open('steve_howe.mp4')
->exportForHLS()
->withRotatingEncryptionKey(function ($filename, $contents) {
$videoId = 1;

// use this callback to store the encryption keys

Storage::disk('secrets')->put($videoId . '/' . $filename, $contents);

// or...

DB::table('hls_secrets')->insert([
'video_id' => $videoId,
'filename' => $filename,
'contents' => $contents,
]);
})
->addFormat($lowBitrate)
->addFormat($midBitrate)
->addFormat($highBitrate)
->save('adaptive_steve.m3u8');
```

The `withRotatingEncryptionKey` method has an optional second argument to set the number of segments that use the same key. This defaults to `1`.

```php
FFMpeg::open('steve_howe.mp4')
->exportForHLS()
->withRotatingEncryptionKey($callable, 10);
```

### Protecting your HLS encryption keys

To make working with encrypted HLS even better, we've added a `DynamicHLSPlaylist` class that modifies playlists on-the-fly and specifically for your application. This way, you can add your authentication and authorization logic. As we're using a plain Laravel controller, you can use features like [Gates](https://laravel.com/docs/master/authorization#gates) and [Middleware](https://laravel.com/docs/master/middleware#introduction).

In this example, we've saved the HLS export to the `public` disk, and we've stored the encryption keys to the `secrets` disk, which isn't publicly available. As the browser can't access the encryption keys, it won't play the video. Each playlist has paths to the encryption keys, and we need to modify those paths to point to an accessible endpoint.

This implementation consists of two routes. One that responses with an encryption key and one that responses with a modified playlist. The first route (`video.key`) is relatively simple, and this is where you should add your additional logic.

The second route (`video.playlist`) uses the `DynamicHLSPlaylist` class. Call the `dynamicHLSPlaylist` method on the `FFMpeg` facade, and similar to opening media files, you can open a playlist utilizing the `fromDisk` and `open` methods. Then you must provide three callbacks. Each of them gives you a relative path and expects a full path in return. As the `DynamicHLSPlaylist` class implements the `Illuminate\Contracts\Support\Responsable` interface, you can return the instance.

The first callback (KeyUrlResolver) gives you the relative path to an encryption key. The second callback (MediaUrlResolver) gives you the relative path to a media segment (.ts files). The third callback (PlaylistUrlResolver) gives you the relative path to a playlist.

Now instead of using `Storage::disk('public')->url('adaptive_steve.m3u8')` to get the full url to your primary playlist, you can use `route('video.playlist', ['playlist' => 'adaptive_steve.m3u8'])`. The `DynamicHLSPlaylist` class takes care of all the paths and urls.

```php
Route::get('/video/secret/{key}', function ($key) {
return Storage::disk('secrets')->download($key);
})->name('video.key');

Route::get('/video/{playlist}', function ($playlist) {
return FFMpeg::dynamicHLSPlaylist()
->fromDisk('public')
->open($playlist)
->setKeyUrlResolver(function ($key) {
return route('video.key', ['key' => $key]);
})
->setMediaUrlResolver(function ($mediaFilename) {
return Storage::disk('public')->url($mediaFilename);
})
->setPlaylistUrlResolver(function ($playlistFilename) {
return route('video.playlist', ['playlist' => $playlistFilename]);
});
})->name('video.playlist');
```

## Process Output

You can get the raw process output by calling the `getProcessOutput` method. Though the use-case is limited, you can use it to analyze a file (for example, with the `volumedetect` filter). It returns a `\ProtoneMedia\LaravelFFMpeg\Support\ProcessOutput` class that has three methods: `all`, `errors` and `output`. Each method returns a line with the corresponding lines.

```php
$processOutput = FFMpeg::open('video.mp4')
->export()
->addFilter(['-filter:a', 'volumedetect', '-f', 'null'])
->getProcessOutput();

$processOutput->all();
$processOutput->errors();
$processOutput->out();
```

## Advanced

The Media object you get when you 'open' a file, actually holds the Media object that belongs to the [underlying driver](https://github.com/PHP-FFMpeg/PHP-FFMpeg). It handles dynamic method calls as you can see [here](https://github.com/pascalbaljetmedia/laravel-ffmpeg/blob/master/src/Media.php#L114-L117). This way all methods of the underlying driver are still available to you.
Expand Down
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@
}
],
"require": {
"php": "^7.2",
"php": "^7.3|^8.0",
"evenement/evenement": "^3.0",
"illuminate/bus": "^6.0|^7.0|^8.0",
"illuminate/config": "^6.0|^7.0|^8.0",
"illuminate/filesystem": "^6.0|^7.0|^8.0",
"illuminate/log": "^6.0|^7.0|^8.0",
"illuminate/support": "^6.0|^7.0|^8.0",
"php-ffmpeg/php-ffmpeg": "^0.16.0"
"php-ffmpeg/php-ffmpeg": "^0.17.0"
},
"require-dev": {
"league/flysystem-memory": "^1.0",
"mockery/mockery": "^1.3",
"mockery/mockery": "^1.3.3",
"orchestra/testbench": "^4.0|^5.0|^6.0",
"phpunit/phpunit": "^8.0",
"phpunit/phpunit": "^8.0|^9.0",
"spatie/image": "^1.7",
"twistor/flysystem-http": "^0.2.0"
},
Expand Down Expand Up @@ -69,4 +69,4 @@
}
}
}
}
}
39 changes: 39 additions & 0 deletions src/Drivers/PHPFFMpeg.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace ProtoneMedia\LaravelFFMpeg\Drivers;

use Alchemy\BinaryDriver\Listeners\ListenerInterface;
use Exception;
use FFMpeg\Coordinate\TimeCode;
use FFMpeg\Driver\FFMpegDriver;
use FFMpeg\FFMpeg;
use FFMpeg\Media\AbstractMediaType;
use FFMpeg\Media\AdvancedMedia as BaseAdvancedMedia;
Expand Down Expand Up @@ -158,6 +160,43 @@ public function openAdvanced(MediaCollection $mediaCollection): self
return $this->open($mediaCollection);
}

/**
* Returns the FFMpegDriver of the underlying library.
*
* @return \FFMpeg\Driver\FFMpegDriver
*/
private function getFFMpegDriver(): FFMpegDriver
{
return $this->get()->getFFMpegDriver();
}

/**
* Add a Listener to the underlying library.
*
* @param \Alchemy\BinaryDriver\Listeners\ListenerInterface $listener
* @return self
*/
public function addListener(ListenerInterface $listener): self
{
$this->getFFMpegDriver()->listen($listener);

return $this;
}

/**
* Add an event handler to the underlying library.
*
* @param string $event
* @param callable $callback
* @return self
*/
public function onEvent(string $event, callable $callback): self
{
$this->getFFMpegDriver()->on($event, $callback);

return $this;
}

/**
* Returns the underlying media object itself.
*/
Expand Down
Loading

0 comments on commit 8df75e9

Please sign in to comment.