protonemedia /
laravel-ffmpeg
| 1 | <?php |
||||||
| 2 | |||||||
| 3 | namespace ProtoneMedia\LaravelFFMpeg\Exporters; |
||||||
| 4 | |||||||
| 5 | use Closure; |
||||||
| 6 | use Illuminate\Filesystem\Filesystem; |
||||||
| 7 | use Illuminate\Support\Collection; |
||||||
| 8 | use ProtoneMedia\LaravelFFMpeg\FFMpeg\StdListener; |
||||||
| 9 | use ProtoneMedia\LaravelFFMpeg\Filesystem\Disk; |
||||||
| 10 | use ProtoneMedia\LaravelFFMpeg\Filesystem\TemporaryDirectories; |
||||||
| 11 | |||||||
| 12 | trait EncryptsHLSSegments |
||||||
| 13 | { |
||||||
| 14 | /** |
||||||
| 15 | * The encryption key. |
||||||
| 16 | * |
||||||
| 17 | * @var string |
||||||
| 18 | */ |
||||||
| 19 | private $encryptionKey; |
||||||
| 20 | |||||||
| 21 | /** |
||||||
| 22 | * The encryption key filename. |
||||||
| 23 | * |
||||||
| 24 | * @var string |
||||||
| 25 | */ |
||||||
| 26 | private $encryptionKeyFilename; |
||||||
| 27 | |||||||
| 28 | /** |
||||||
| 29 | * Gets called whenever a new encryption key is set. |
||||||
| 30 | * |
||||||
| 31 | * @var callable |
||||||
| 32 | */ |
||||||
| 33 | private $onNewEncryptionKey; |
||||||
| 34 | |||||||
| 35 | /** |
||||||
| 36 | * Disk to store the secrets. |
||||||
| 37 | */ |
||||||
| 38 | private $encryptionSecretsRoot; |
||||||
| 39 | |||||||
| 40 | /** |
||||||
| 41 | * Encryption IV |
||||||
| 42 | * |
||||||
| 43 | * @var string |
||||||
| 44 | */ |
||||||
| 45 | private $encryptionIV; |
||||||
| 46 | |||||||
| 47 | /** |
||||||
| 48 | * Wether to rotate the key on every segment. |
||||||
| 49 | * |
||||||
| 50 | * @var boolean |
||||||
| 51 | */ |
||||||
| 52 | private $rotateEncryptiongKey = false; |
||||||
| 53 | |||||||
| 54 | /** |
||||||
| 55 | * Number of opened segments. |
||||||
| 56 | * |
||||||
| 57 | * @var integer |
||||||
| 58 | */ |
||||||
| 59 | private $segmentsOpened = 0; |
||||||
| 60 | |||||||
| 61 | /** |
||||||
| 62 | * Number of segments that can use the same key. |
||||||
| 63 | * |
||||||
| 64 | * @var integer |
||||||
| 65 | */ |
||||||
| 66 | private $segmentsPerKey = 1; |
||||||
| 67 | |||||||
| 68 | /** |
||||||
| 69 | * Listener that will rotate the key. |
||||||
| 70 | * |
||||||
| 71 | * @var \ProtoneMedia\LaravelFFMpeg\FFMpeg\StdListener |
||||||
| 72 | */ |
||||||
| 73 | private $listener; |
||||||
| 74 | |||||||
| 75 | /** |
||||||
| 76 | * A fresh filename and encryption key for the next round. |
||||||
| 77 | * |
||||||
| 78 | * @var array |
||||||
| 79 | */ |
||||||
| 80 | private $nextEncryptionFilenameAndKey; |
||||||
| 81 | |||||||
| 82 | /** |
||||||
| 83 | * Creates a new encryption key. |
||||||
| 84 | * |
||||||
| 85 | * @return string |
||||||
| 86 | */ |
||||||
| 87 | public static function generateEncryptionKey(): string |
||||||
| 88 | { |
||||||
| 89 | return random_bytes(16); |
||||||
| 90 | } |
||||||
| 91 | /** |
||||||
| 92 | * Creates a new encryption key filename. |
||||||
| 93 | * |
||||||
| 94 | * @return string |
||||||
| 95 | */ |
||||||
| 96 | public static function generateEncryptionKeyFilename(): string |
||||||
| 97 | { |
||||||
| 98 | return bin2hex(random_bytes(8)) . '.key'; |
||||||
| 99 | } |
||||||
| 100 | |||||||
| 101 | /** |
||||||
| 102 | * Initialises the disk, info and IV for encryption and sets the key. |
||||||
| 103 | * |
||||||
| 104 | * @param string $key |
||||||
| 105 | * @param string $filename |
||||||
| 106 | * @return self |
||||||
| 107 | */ |
||||||
| 108 | public function withEncryptionKey($key, $filename = 'secret.key'): self |
||||||
| 109 | { |
||||||
| 110 | $this->encryptionKey = $key; |
||||||
| 111 | $this->encryptionIV = bin2hex(static::generateEncryptionKey()); |
||||||
| 112 | |||||||
| 113 | $this->encryptionKeyFilename = $filename; |
||||||
| 114 | $this->encryptionSecretsRoot = app(TemporaryDirectories::class)->create(); |
||||||
| 115 | |||||||
| 116 | return $this; |
||||||
| 117 | } |
||||||
| 118 | |||||||
| 119 | /** |
||||||
| 120 | * Enables encryption with rotating keys. The callable will receive every new |
||||||
| 121 | * key and the integer sets the number of segments that can |
||||||
| 122 | * use the same key. |
||||||
| 123 | * |
||||||
| 124 | * @param Closure $callback |
||||||
| 125 | * @param int $segmentsPerKey |
||||||
| 126 | * @return self |
||||||
| 127 | */ |
||||||
| 128 | public function withRotatingEncryptionKey(Closure $callback, int $segmentsPerKey = 1): self |
||||||
| 129 | { |
||||||
| 130 | $this->rotateEncryptiongKey = true; |
||||||
| 131 | $this->onNewEncryptionKey = $callback; |
||||||
| 132 | $this->segmentsPerKey = $segmentsPerKey; |
||||||
| 133 | |||||||
| 134 | return $this->withEncryptionKey(null, null); |
||||||
| 135 | } |
||||||
| 136 | |||||||
| 137 | /** |
||||||
| 138 | * Rotates the key and returns the absolute path to the info file. This method |
||||||
| 139 | * should be executed as fast as possible, or we might be too late for FFmpeg |
||||||
| 140 | * opening the next segment. That's why we don't use the Disk-class magic. |
||||||
| 141 | * |
||||||
| 142 | * @return string |
||||||
| 143 | */ |
||||||
| 144 | private function rotateEncryptionKey(): string |
||||||
| 145 | { |
||||||
| 146 | if ($this->nextEncryptionFilenameAndKey) { |
||||||
|
0 ignored issues
–
show
|
|||||||
| 147 | [$keyFilename, $encryptionKey] = $this->nextEncryptionFilenameAndKey; |
||||||
| 148 | } else { |
||||||
| 149 | $keyFilename = $this->encryptionKeyFilename ?: static::generateEncryptionKeyFilename(); |
||||||
| 150 | $encryptionKey = $this->encryptionKey ?: static::generateEncryptionKey(); |
||||||
| 151 | } |
||||||
| 152 | |||||||
| 153 | // get the absolute path to the info file and encryption key |
||||||
| 154 | $hlsKeyInfoPath = $this->encryptionSecretsRoot . '/' . HLSExporter::HLS_KEY_INFO_FILENAME; |
||||||
| 155 | $keyPath = $this->encryptionSecretsRoot . '/' . $keyFilename; |
||||||
| 156 | |||||||
| 157 | $normalizedKeyPath = Disk::normalizePath($keyPath); |
||||||
| 158 | |||||||
| 159 | // store the encryption key |
||||||
| 160 | file_put_contents($keyPath, $encryptionKey); |
||||||
| 161 | |||||||
| 162 | // store an info file with a reference to the encryption key and IV |
||||||
| 163 | file_put_contents( |
||||||
| 164 | $hlsKeyInfoPath, |
||||||
| 165 | $normalizedKeyPath . PHP_EOL . $normalizedKeyPath . PHP_EOL . $this->encryptionIV |
||||||
| 166 | ); |
||||||
| 167 | |||||||
| 168 | // prepare for the next round |
||||||
| 169 | if ($this->rotateEncryptiongKey) { |
||||||
| 170 | $this->nextEncryptionFilenameAndKey = [ |
||||||
| 171 | static::generateEncryptionKeyFilename(), |
||||||
| 172 | static::generateEncryptionKey(), |
||||||
| 173 | ]; |
||||||
| 174 | } |
||||||
| 175 | |||||||
| 176 | // call the callback |
||||||
| 177 | if ($this->onNewEncryptionKey) { |
||||||
| 178 | call_user_func($this->onNewEncryptionKey, $keyFilename, $encryptionKey, $this->listener); |
||||||
| 179 | } |
||||||
| 180 | |||||||
| 181 | // return the absolute path to the info file |
||||||
| 182 | return Disk::normalizePath($hlsKeyInfoPath); |
||||||
| 183 | } |
||||||
| 184 | |||||||
| 185 | /** |
||||||
| 186 | * Returns an array with the encryption parameters. |
||||||
| 187 | * |
||||||
| 188 | * @return array |
||||||
| 189 | */ |
||||||
| 190 | private function getEncrypedHLSParameters(): array |
||||||
| 191 | { |
||||||
| 192 | if (!$this->encryptionIV) { |
||||||
| 193 | return []; |
||||||
| 194 | } |
||||||
| 195 | |||||||
| 196 | $keyInfoPath = $this->rotateEncryptionKey(); |
||||||
| 197 | $parameters = ['-hls_key_info_file', $keyInfoPath]; |
||||||
| 198 | |||||||
| 199 | if ($this->rotateEncryptiongKey) { |
||||||
| 200 | $parameters[] = '-hls_flags'; |
||||||
| 201 | $parameters[] = 'periodic_rekey'; |
||||||
| 202 | } |
||||||
| 203 | |||||||
| 204 | return $parameters; |
||||||
| 205 | } |
||||||
| 206 | |||||||
| 207 | /** |
||||||
| 208 | * Adds a listener and handler to rotate the key on |
||||||
| 209 | * every new HLS segment. |
||||||
| 210 | * |
||||||
| 211 | * @return void |
||||||
| 212 | */ |
||||||
| 213 | private function addHandlerToRotateEncryptionKey() |
||||||
| 214 | { |
||||||
| 215 | if (!$this->rotateEncryptiongKey) { |
||||||
| 216 | return; |
||||||
| 217 | } |
||||||
| 218 | |||||||
| 219 | $this->listener = new StdListener(HLSExporter::ENCRYPTION_LISTENER); |
||||||
| 220 | |||||||
| 221 | $this->addListener($this->listener) |
||||||
|
0 ignored issues
–
show
It seems like
addListener() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 222 | ->onEvent(HLSExporter::ENCRYPTION_LISTENER, function ($line) { |
||||||
| 223 | if (!strpos($line, ".keyinfo' for reading")) { |
||||||
| 224 | return; |
||||||
| 225 | } |
||||||
| 226 | |||||||
| 227 | $this->segmentsOpened++; |
||||||
| 228 | |||||||
| 229 | if ($this->segmentsOpened % $this->segmentsPerKey === 0) { |
||||||
| 230 | $this->rotateEncryptionKey(); |
||||||
| 231 | } |
||||||
| 232 | }); |
||||||
| 233 | } |
||||||
| 234 | |||||||
| 235 | /** |
||||||
| 236 | * Remove the listener at the end of the export to |
||||||
| 237 | * prevent duplicate event handlers. |
||||||
| 238 | * |
||||||
| 239 | * @return self |
||||||
| 240 | */ |
||||||
| 241 | private function removeHandlerThatRotatesEncryptionKey(): self |
||||||
| 242 | { |
||||||
| 243 | if ($this->listener) { |
||||||
| 244 | $this->listener->removeAllListeners(); |
||||||
| 245 | $this->removeListener($this->listener); |
||||||
|
0 ignored issues
–
show
It seems like
removeListener() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 246 | $this->listener = null; |
||||||
| 247 | |||||||
| 248 | $this->getFFMpegDriver()->removeAllListeners(HLSExporter::ENCRYPTION_LISTENER); |
||||||
|
0 ignored issues
–
show
It seems like
getFFMpegDriver() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 249 | } |
||||||
| 250 | |||||||
| 251 | return $this; |
||||||
| 252 | } |
||||||
| 253 | |||||||
| 254 | /** |
||||||
| 255 | * While encoding, the encryption keys are saved to a temporary directory. |
||||||
| 256 | * With this method, we loop through all segment playlists and replace |
||||||
| 257 | * the absolute path to the keys to a relative ones. |
||||||
| 258 | * |
||||||
| 259 | * @param \Illuminate\Support\Collection $playlistMedia |
||||||
| 260 | * @return self |
||||||
| 261 | */ |
||||||
| 262 | private function replaceAbsolutePathsHLSEncryption(Collection $playlistMedia): self |
||||||
| 263 | { |
||||||
| 264 | if (!$this->encryptionSecretsRoot) { |
||||||
| 265 | return $this; |
||||||
| 266 | } |
||||||
| 267 | |||||||
| 268 | $playlistMedia->each(function ($playlistMedia) { |
||||||
| 269 | $disk = $playlistMedia->getDisk(); |
||||||
| 270 | $path = $playlistMedia->getPath(); |
||||||
| 271 | |||||||
| 272 | $prefix = '#EXT-X-KEY:METHOD=AES-128,URI="'; |
||||||
| 273 | |||||||
| 274 | $content = str_replace( |
||||||
| 275 | $prefix . Disk::normalizePath($this->encryptionSecretsRoot) . '/', |
||||||
| 276 | $prefix, |
||||||
| 277 | $disk->get($path) |
||||||
| 278 | ); |
||||||
| 279 | |||||||
| 280 | $disk->put($path, $content); |
||||||
| 281 | }); |
||||||
| 282 | |||||||
| 283 | return $this; |
||||||
| 284 | } |
||||||
| 285 | |||||||
| 286 | /** |
||||||
| 287 | * Removes the encryption keys from the temporary disk. |
||||||
| 288 | * |
||||||
| 289 | * @return self |
||||||
| 290 | */ |
||||||
| 291 | private function cleanupHLSEncryption(): self |
||||||
| 292 | { |
||||||
| 293 | if ($this->encryptionSecretsRoot) { |
||||||
| 294 | (new Filesystem)->deleteDirectory($this->encryptionSecretsRoot); |
||||||
| 295 | } |
||||||
| 296 | |||||||
| 297 | return $this; |
||||||
| 298 | } |
||||||
| 299 | } |
||||||
| 300 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)or! empty(...)instead.