├── .gitignore ├── src ├── Exceptions │ ├── NoCredentialsException.php │ └── NoPrivateKeyOrTokenException.php ├── config │ └── cloudflare-stream.php ├── CloudflareStreamLaravel.php ├── CloudflareStreamServiceProvider.php └── CloudflareStream.php ├── .editorconfig ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | -------------------------------------------------------------------------------- /src/Exceptions/NoCredentialsException.php: -------------------------------------------------------------------------------- 1 | env('CLOUDFLARE_STREAM_ACCOUNT_ID'), 5 | 'authKey' => env('CLOUDFLARE_STREAM_AUTH_KEY'), 6 | 'authEMail' => env('CLOUDFLARE_STREAM_AUTH_EMAIL'), 7 | 'privateKeyId' => env('CLOUDFLARE_STREAM_PRIVATE_KEY_ID'), 8 | 'privateKeyToken' => env('CLOUDFLARE_STREAM_PRIVATE_KEY_TOKEN'), 9 | ]; 10 | -------------------------------------------------------------------------------- /src/CloudflareStreamLaravel.php: -------------------------------------------------------------------------------- 1 | publishes([ 15 | __DIR__ . '/config/cloudflare-stream.php' => config_path('cloudflare-stream.php'), 16 | ]); 17 | } 18 | 19 | /** 20 | * Register. 21 | */ 22 | public function register() 23 | { 24 | // 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "afloeter/laravel-cloudflare-stream", 3 | "description": "Manage the Cloudflare Stream API with ease.", 4 | "authors": [ 5 | { 6 | "name": "Alexander Flöter", 7 | "email": "af@mediaguy.de" 8 | } 9 | ], 10 | "license": "MIT", 11 | "minimum-stability": "dev", 12 | "autoload": { 13 | "psr-4": { 14 | "AFloeter\\CloudflareStream\\": "src/" 15 | } 16 | }, 17 | "require": { 18 | "ext-json": "*", 19 | "firebase/php-jwt": "^5.2", 20 | "guzzlehttp/guzzle": "^7.2", 21 | "laravel/framework": "^9.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexander Flöter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-cloudflare-stream 2 | Manage Cloudflare Stream with ease by using this handy PHP API wrapper. The `laravel-cloudflare-stream` package gives ability to... 3 | 4 | * ✓ **List your videos** 5 | * optionally using parameters to filter results 6 | * *after* 7 | * *before* 8 | * *include_counts* 9 | * *search* 10 | * *limit* 11 | * *asc* 12 | * *status* 13 | * ✓ **Details of your videos** 14 | * Meta information (read / write) 15 | * Video name (read / write) 16 | * Require signed URLs (read-only) 17 | * Width and height (read-only) 18 | * ✓ **Get embed code of your videos** 19 | * With or without signed URLs 20 | * Add attributes to embed code 21 | * *controls* 22 | * ✓ **Get playback URLs of your videos** 23 | * With or without signed token 24 | * ✓ **Generate signed tokens for your videos** 25 | * ✓ **Delete your videos** 26 | 27 | Feel free to check out the Cloudflare Stream [documentation](https://developers.cloudflare.com/stream/) and [API documentation](https://api.cloudflare.com/#stream-videos-properties) for further information. 28 | 29 | ## Installation 30 | 31 | ### Step 1: Install using Composer 32 | Add the following to your root `composer.json` and install with `composer install` or `composer update`. 33 | 34 | { 35 | "require": { 36 | "afloeter/laravel-cloudflare-stream": "~1.0.0" 37 | } 38 | }, 39 | "repositories": [ 40 | { 41 | "type": "vcs", 42 | "url": "https://github.com/afloeter/laravel-cloudflare-stream" 43 | } 44 | ] 45 | 46 | ...or use `composer require afloeter/laravel-cloudflare-stream` in your console after just adding the repository to your composer.json file. 47 | 48 | ### Step 2: Publish the config file for Laravel projects 49 | Publish the config file with `php artisan vendor:publish --provider="AFloeter\CloudflareStreamServiceProvider"`. 50 | 51 | ### Step 3: Add informationen to Laravel's `.env` file 52 | Add the following lines to your root `.env` file of your Laravel instance. 53 | 54 | CLOUDFLARE_STREAM_ACCOUNT_ID= 55 | CLOUDFLARE_STREAM_AUTH_KEY= 56 | CLOUDFLARE_STREAM_AUTH_EMAIL= 57 | CLOUDFLARE_STREAM_PRIVATE_KEY_ID= 58 | CLOUDFLARE_STREAM_PRIVATE_KEY_TOKEN= 59 | 60 | Complete the following information. 61 | 62 | * `CLOUDFLARE_STREAM_ACCOUNT_ID` is your [Cloudflare account](https://dash.cloudflare.com/) ID. 63 | * `CLOUDFLARE_STREAM_AUTH_KEY` is your [Cloudflare API key](https://dash.cloudflare.com/profile/api-tokens). 64 | * `CLOUDFLARE_STREAM_AUTH_EMAIL` is the email address of your [Cloudflare account](https://dash.cloudflare.com/profile). 65 | 66 | Leave `CLOUDFLARE_STREAM_PRIVATE_KEY_ID` and `CLOUDFLARE_STREAM_PRIVATE_KEY_TOKEN` blank if you don't use signed URLs at all. 67 | 68 | * `CLOUDFLARE_STREAM_PRIVATE_KEY_ID` is the ID of your signing key 69 | * `CLOUDFLARE_STREAM_PRIVATE_KEY_TOKEN` is the related RSA private key. 70 | 71 | Otherwise: Check the [documentation](https://developers.cloudflare.com/stream/security/signed-urls/) on [how to create a signing key and get RSA private key](https://developers.cloudflare.com/stream/security/signed-urls/#creating-a-signing-key) in PEM format. 72 | 73 | ## Usage 74 | 75 | ### Laravel 76 | If you have done the `vendor:publish` step, your credentials will be grabbed from the `config/cloudflare-stream.php` and / or `.env` file. So, you can use `CloudflareStreamLaravel()` without providing your information once again. 77 | 78 | use AFloeter\CloudflareStream\CloudflareStreamLaravel; 79 | 80 | ... 81 | 82 | $cfs = new CloudflareStreamLaravel(); 83 | $listOfVideos = $cfs->list(); 84 | 85 | ... 86 | 87 | ### Generic PHP 88 | If you are on composer-enabled projects use `CloudflareStream()`. Without composer try requiring `src/CloudflareStream.php` directly into your project. 89 | 90 | use AFloeter\CloudflareStream\CloudflareStream; 91 | 92 | ... 93 | 94 | $cfs = new CloudflareStream($accountId, $authKey, $authEMail); 95 | $listOfVideos = $cfs->list(); 96 | 97 | ... 98 | 99 | If you are using signed URLs for your videos, simply add the `$privateKey` and `$privateKeyToken` variables. 100 | 101 | use AFloeter\CloudflareStream\CloudflareStream; 102 | 103 | ... 104 | 105 | $cfs = new CloudflareStream($accountId, $authKey, $authEMail, $privateKey, $privateKeyToken); 106 | $signedToken = $cfs->getSignedToken($videoId); 107 | 108 | ... 109 | 110 | ## To Do 111 | It's planned to add support to... 112 | 113 | * **Upload a video** 114 | * From a URL 115 | * Using a single HTTP request 116 | * Using `ankitpokhrel/tus-php` 117 | * **User uploads** 118 | * Create a video and get authenticated direct upload URL 119 | * **Create and revoke signing keys.** 120 | * **Add, get and remove `.vtt` caption files.** 121 | * **Set, get and remove allowed origins** 122 | 123 | ## Changelog 124 | 125 | All notable changes to `laravel-cloudflare-stream` will be documented here. 126 | 127 | ### 1.0.0 - 2020-06-12 128 | * initial release 129 | 130 | ## License 131 | `laravel-cloudflare-stream` is distributed under the terms of the [MIT License](LICENSE). 132 | -------------------------------------------------------------------------------- /src/CloudflareStream.php: -------------------------------------------------------------------------------- 1 | accountId = $accountId; 39 | $this->authKey = $authKey; 40 | $this->authEMail = $authEMail; 41 | $this->guzzle = new Client([ 42 | 'base_uri' => 'https://api.cloudflare.com/client/v4/' 43 | ]); 44 | 45 | if (!empty($privateKey) && !empty($privateKeyToken)) { 46 | $this->privateKeyId = $privateKey; 47 | $this->privateKeyToken = $privateKeyToken; 48 | } 49 | } 50 | 51 | /** 52 | * Get a list of videos. 53 | * 54 | * @param array $customParameters 55 | * @return string 56 | * @throws GuzzleException 57 | */ 58 | public function list($customParameters = []) 59 | { 60 | // Define standard parameters 61 | $parameters = [ 62 | 'include_counts' => true, 63 | 'limit' => 1000, 64 | 'asc' => false 65 | ]; 66 | 67 | // Set custom parameters 68 | if (!empty($customParameters) && is_array($customParameters)) { 69 | $parameters = array_merge($parameters, $customParameters); 70 | } 71 | 72 | return $this->request('accounts/' . $this->accountId . '/stream?' . http_build_query($parameters))->getBody()->getContents(); 73 | } 74 | 75 | /** 76 | * Fetch details of a single video. 77 | * 78 | * @param string $uid 79 | * @return string 80 | * @throws GuzzleException 81 | */ 82 | public function video(string $uid) 83 | { 84 | return $this->request('accounts/' . $this->accountId . '/stream/' . $uid)->getBody()->getContents(); 85 | } 86 | 87 | /** 88 | * Get embed code. Could be returned with signed token if necessary. 89 | * 90 | * @param string $uid 91 | * @param bool $addControls 92 | * @param bool $useSignedToken 93 | * @return string 94 | * @throws NoPrivateKeyOrTokenException|GuzzleException 95 | */ 96 | public function embed(string $uid, bool $addControls = false, bool $useSignedToken = true) 97 | { 98 | $embed = $this->request('accounts/' . $this->accountId . '/stream/' . $uid . '/embed')->getBody(); 99 | $requireSignedToken = false; 100 | 101 | // Require signed token? 102 | if ($useSignedToken) { 103 | $video = json_decode($this->video($uid), true); 104 | $requireSignedToken = $video['result']['requireSignedURLs']; 105 | } 106 | 107 | // Add controls attribute? 108 | if ($addControls) { 109 | return str_replace('src="' . $uid . '"', 'src="' . ($useSignedToken && $requireSignedToken ? $this->getSignedToken($uid) : $uid) . '" controls', $embed); 110 | } 111 | 112 | // Signed URL necessary? 113 | if ($useSignedToken && $requireSignedToken) { 114 | return str_replace('src="' . $uid . '"', 'src="' . $this->getSignedToken($uid) . '"', $embed); 115 | } 116 | 117 | // Return embed code 118 | return $embed; 119 | } 120 | 121 | /** 122 | * Delete video. 123 | * 124 | * @param string $uid 125 | * @return string 126 | * @throws GuzzleException 127 | */ 128 | public function delete(string $uid) 129 | { 130 | return $this->request('accounts/' . $this->accountId . '/stream/' . $uid, 'delete')->getBody()->getContents(); 131 | } 132 | 133 | /** 134 | * Get meta data for a specific video. 135 | * 136 | * @param string $uid 137 | * @return array 138 | * @throws GuzzleException 139 | */ 140 | public function getMeta(string $uid) 141 | { 142 | 143 | // Get all data 144 | $data = json_decode($this->video($uid), true); 145 | 146 | // Return meta data 147 | return $data['result']['meta']; 148 | } 149 | 150 | /** 151 | * Set meta data for a specific video. 152 | * 153 | * @param string $uid 154 | * @param array $meta 155 | * @return string 156 | * @throws GuzzleException 157 | */ 158 | public function setMeta(string $uid, array $meta) 159 | { 160 | // Merge meta data 161 | $meta = [ 162 | 'meta' => array_merge($this->getMeta($uid), $meta) 163 | ]; 164 | 165 | // Request 166 | $response = $this->request('accounts/' . $this->accountId . '/stream/' . $uid, 'post', $meta); 167 | 168 | // Return result 169 | return $response->getBody()->getContents(); 170 | } 171 | 172 | /** 173 | * Remove meta data by key. 174 | * 175 | * @param string $uid 176 | * @param string $metaKey 177 | * @return string 178 | * @throws GuzzleException 179 | */ 180 | public function removeMeta(string $uid, string $metaKey) 181 | { 182 | 183 | // Merge meta data 184 | $meta = [ 185 | 'meta' => array_merge($this->getMeta($uid)) 186 | ]; 187 | 188 | // Remove key 189 | if (array_key_exists($metaKey, $meta['meta'])) { 190 | unset($meta['meta'][$metaKey]); 191 | } 192 | 193 | // Request 194 | return $this->request('accounts/' . $this->accountId . '/stream/' . $uid, 'post', $meta)->getBody()->getContents(); 195 | 196 | } 197 | 198 | /** 199 | * Get name of a video. 200 | * 201 | * @param string $uid 202 | * @return string 203 | * @throws GuzzleException 204 | */ 205 | public function getName(string $uid) 206 | { 207 | $meta = $this->getMeta($uid); 208 | return $meta['name']; 209 | } 210 | 211 | /** 212 | * Rename a video. 213 | * 214 | * @param string $uid 215 | * @param string $name 216 | * @return string 217 | * @throws GuzzleException 218 | */ 219 | public function setName(string $uid, string $name) 220 | { 221 | return $this->setMeta($uid, ['name' => $name]); 222 | } 223 | 224 | /** 225 | * Set whether a specific video requires signed URLs or not. 226 | * 227 | * @param string $uid 228 | * @param bool $required 229 | * @return string 230 | * @throws GuzzleException 231 | */ 232 | public function setSignedURLs(string $uid, bool $required) 233 | { 234 | return $this->request('accounts/' . $this->accountId . '/stream/' . $uid, 'post', [ 235 | 'uid' => $uid, 236 | 'requireSignedURLS' => $required 237 | ])->getBody()->getContents(); 238 | } 239 | 240 | /** 241 | * Get playback URLs of a specific video. 242 | * 243 | * @param string $uid 244 | * @param bool $useSignedToken 245 | * @return string 246 | * @throws GuzzleException 247 | * @throws NoPrivateKeyOrTokenException 248 | */ 249 | public function getPlaybackURLs(string $uid, $useSignedToken = true) 250 | { 251 | 252 | // Get all data 253 | $video = json_decode($this->video($uid), true); 254 | 255 | // Signed URL necessary? 256 | if ($useSignedToken && $video['result']['requireSignedURLs']) { 257 | 258 | // Replace uid with signed token 259 | foreach ($video['result']['playback'] as $key => $value) { 260 | $video['result']['playback'][$key] = str_replace($uid, $this->getSignedToken($uid), $value); 261 | } 262 | 263 | } 264 | 265 | // Return playback URLs 266 | return json_encode($video['result']['playback']); 267 | 268 | } 269 | 270 | /** 271 | * Get signed token for a video. 272 | * 273 | * @param string $uid 274 | * @param int $addHours 275 | * @return string 276 | * @throws NoPrivateKeyOrTokenException 277 | */ 278 | public function getSignedToken(string $uid, int $addHours = 4) 279 | { 280 | if (empty($this->privateKeyId) || empty($this->privateKeyToken)) { 281 | throw new NoPrivateKeyOrTokenException(); 282 | } 283 | 284 | return JWT::encode([ 285 | 'kid' => $this->privateKeyId, 286 | 'sub' => $uid, 287 | "exp" => time() + ($addHours * 60 * 60) 288 | ], base64_decode($this->privateKeyToken), 'RS256'); 289 | } 290 | 291 | /** 292 | * Get width and height of a video. 293 | * 294 | * @param string $uid 295 | * @return string 296 | * @throws GuzzleException 297 | */ 298 | public function getDimensions(string $uid) 299 | { 300 | // Get all data 301 | $video = json_decode($this->video($uid), true); 302 | 303 | // Return playback URLs 304 | return json_encode($video['result']['input']); 305 | } 306 | 307 | /** 308 | * Request wrapper function. 309 | * 310 | * @param string $endpoint 311 | * @param string $method 312 | * @param array $data 313 | * @return ResponseInterface 314 | * @throws GuzzleException 315 | */ 316 | private function request(string $endpoint, $method = 'get', $data = []) 317 | { 318 | // Define headers. 319 | $headers = [ 320 | 'X-Auth-Key' => $this->authKey, 321 | 'X-Auth-Email' => $this->authEMail, 322 | 'Content-Type' => 'application/json' 323 | ]; 324 | 325 | // Define options for post request method... 326 | if (count($data) && $method === "post") { 327 | $options = [ 328 | 'headers' => $headers, 329 | RequestOptions::JSON => $data 330 | ]; 331 | } // ...or define options for all other request methods 332 | else { 333 | $options = [ 334 | 'headers' => $headers, 335 | ]; 336 | } 337 | 338 | return $this->guzzle->request($method, $endpoint, $options); 339 | } 340 | } 341 | --------------------------------------------------------------------------------