LiveStream::getEvents()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 9
c 1
b 0
f 0
nc 3
nop 5
dl 0
loc 22
rs 9.9666
1
<?php
2
3
namespace LiveStream;
4
5
use CURLFile;
6
use Exception;
7
8
use LiveStream\Resources\Event;
9
use LiveStream\Resources\RTMPKey;
10
use LiveStream\Resources\Account;
11
use LiveStream\Interfaces\Resource;
12
13
use LiveStream\Exceptions\LiveStreamException;
14
use LiveStream\Exceptions\InValidResourceException;
15
use LiveStream\Resources\Video;
16
17
class LiveStream
18
{
19
    /**
20
     * Live Stream API Base URL.
21
     */
22
    const BASE_URL = 'https://livestreamapis.com/v3/';
23
24
    /**
25
     * LiveStream API HTTP Codes.
26
     */
27
    const ERROR_CODES = [
28
        400 => 'Bad Request: Your request is not properly constructed.',
29
        401 => 'Unauthorized: Your API key is incorrect.',
30
        403 => 'Forbidden:',
31
        405 => 'Method Not Allowed: You tried to access a resource with an invalid method.',
32
        406 => 'Not Acceptable: You requested a format that is not JSON.',
33
        500 => 'Internal Server Error: We had a problem with our server. Please try again later.',
34
        503 => 'Service Unavailable: We are temporarily offline for maintanance. Please try again later.'
35
    ];
36
37
    /**
38
     * API key.
39
     *
40
     * @var string
41
     */
42
    private $apiKey;
43
44
    /**
45
     * Client ID.
46
     *
47
     * @var int
48
     */
49
    private $clientId;
50
51
    /**
52
     * Scope.
53
     *
54
     * @var string
55
     */
56
    private $scope;
57
58
    /**
59
     * Secure access token.
60
     *
61
     * @var string
62
     */
63
    private $token;
64
65
    /**
66
     * Secure access token timestamp.
67
     *
68
     * @var int
69
     */
70
    private $tokenTimestamp;
71
72
    /**
73
     * Class Constructor.
74
     *
75
     * When only an API key is provided key auth method is used. When an API
76
     * key, client ID, and scope are provided secure token auth is used.
77
     *
78
     * @param string $apiKey
79
     * @param int|null $clientId
80
     * @param string|null $scope
81
     *   Valid scopes are: all, readonly, playback
82
     *
83
     * @see https://livestream.com/developers/docs/api/#authentication
84
     */
85
    public function __construct(
86
        string $apiKey,
87
        ?int $clientId = null,
88
        ?string $scope = null
89
    )
90
    {
91
        $this->apiKey = $apiKey;
92
        $this->clientId = $clientId;
93
        $this->scope = $scope;
94
95
        if ($this->clientId !== null && $this->scope !== null) {
96
            $this->refreshToken();
97
        }
98
    }
99
100
    /**
101
     * Get Linked LiveStream Accounts
102
     *
103
     * @return array Array of LiveStream Accounts
104
     */
105
    public function getAccounts(): array
106
    {
107
        $response = $this->request('accounts');
108
109
        $accounts = [];
110
111
        foreach (json_decode($response) as $account) {
112
            $accounts[] = Account::fromObject($account);
113
        }
114
115
        return $accounts;
116
    }
117
118
    /**
119
     * Get Specific Account
120
     *
121
     * @param  integer $accountId
122
     * @return Account|null
123
     */
124
    public function getAccount(int $accountId): ?Account
125
    {
126
        $response = $this->request("accounts/$accountId");
127
        if ($response === null) return null;
128
129
        return Account::fromObject(json_decode($response));
130
    }
131
132
    /**
133
     * Create New Event
134
     *
135
     * @param  integer $accountId
136
     * @param  Event $event
137
     * @return boolean
138
     */
139
    public function createEvent(int $accountId, Event &$event): bool
140
    {
141
        $event->validate();
142
143
        $response = $this->request("accounts/$accountId/events", 'post', $event);
144
145
        if ($response === null) return false;
146
147
        $event = Event::fromObject(json_decode($response));
148
149
        return true;
150
    }
151
152
    /**
153
     * Update Event.
154
     *
155
     * @param  integer $accountId
156
     * @param  LiveStream\Resources\Event $event
0 ignored issues
show
Bug introduced by
The type LiveStream\LiveStream\Resources\Event was not found. Did you mean LiveStream\Resources\Event? If so, make sure to prefix the type with \.
Loading history...
157
     * @return boolean
158
     */
159
    public function updateEvent(int $accountId, Event $event): bool
160
    {
161
        $event->validate(true);
162
163
        $response = $this->request("accounts/$accountId/events/$event->id", 'put', $event);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on LiveStream\Resources\Event. Since you implemented __get, consider adding a @property annotation.
Loading history...
164
165
        if ($response === null) return false;
166
167
        return true;
168
    }
169
170
    /**
171
     * Delete Event
172
     *
173
     * @param  integer $accountId
174
     * @param  integer $eventId
175
     *
176
     * @return boolean
177
     */
178
    public function deleteEvent(int $accountId, int $eventId): ?Event
179
    {
180
        $response = $this->request("accounts/$accountId/events/$eventId", 'delete');
181
182
        if ($response === null) return null;
183
184
        return Event::fromObject(json_decode($response));
0 ignored issues
show
Bug Best Practice introduced by
The expression return LiveStream\Resour...json_decode($response)) returns the type LiveStream\Resources\Event which is incompatible with the documented return type boolean.
Loading history...
185
    }
186
187
    /**
188
     * Update Event Logo.
189
     *
190
     * @param  integer $accountId
191
     * @param  integer $eventId
192
     * @param  string  $filePath
193
     * @return boolean
194
     */
195
    public function updateEventLogo(int $accountId, int $eventId, string $filePath): Event
196
    {
197
        if (!in_array(mime_content_type($filePath), [
198
            'image/png',
199
            'image/jpg',
200
            'image/jpeg',
201
            'image/gif'
202
        ])) {
203
            throw new InValidResourceException('Logo', 'poster (MIME Type must be one of image/png, image/jpg, image/gif)');
204
        }
205
206
        $response = $this->request("accounts/$accountId/events/$eventId/logo", 'put', null, null, [
207
            'logo' => new CURLFile($filePath, mime_content_type($filePath), basename($filePath))
208
        ]);
209
210
        if ($response == null) return null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return null returns the type null which is incompatible with the type-hinted return LiveStream\Resources\Event.
Loading history...
211
212
        return Event::fromObject(json_decode($response));
0 ignored issues
show
Bug Best Practice introduced by
The expression return LiveStream\Resour...json_decode($response)) returns the type LiveStream\Resources\Event which is incompatible with the documented return type boolean.
Loading history...
213
    }
214
215
    /**
216
     * Gets Events by type.
217
     *
218
     * @param string $type
219
     *   Valid values: draft, past, private, or upcoming.
220
     * @param int $accountId
221
     * @param int $page
222
     * @param int $maxItems
223
     * @param string $order
224
     *
225
     * @return array
226
     *
227
     * @throws \LiveStream\Exceptions\LiveStreamException
228
     */
229
    protected function getEvents(
230
        string $type,
231
        int $accountId,
232
        int $page,
233
        int $maxItems,
234
        string $order
235
    ) {
236
        $events = [];
237
238
        $response = $this->request("accounts/{$accountId}/{$type}_events", 'get', null, [
239
          'page'     => $page,
240
          'maxItems' => $maxItems,
241
          'order'    => $order
242
        ]);
243
244
        if ($response === null) return $events;
245
246
        foreach (json_decode($response)->data as $event) {
247
            $events[] = Event::fromObject($event);
248
        }
249
250
        return $events;
251
    }
252
253
    /**
254
     * Get Draft Events
255
     *
256
     * @param integer $accountId
257
     * @param integer $page
258
     * @param integer $maxItems
259
     * @param string $order
260
     *
261
     * @return array
262
     *
263
     * @throws \LiveStream\Exceptions\LiveStreamException
264
     *
265
     * @see https://livestream.com/developers/docs/api/#get-draft-events
266
     */
267
    public function getDraftEvents(
268
        int $accountId,
269
        int $page = 1,
270
        int $maxItems = 10,
271
        string $order = 'desc'
272
    ): array {
273
        return $this->getEvents('draft', $accountId, $page, $maxItems, $order);
274
    }
275
276
    /**
277
     * Get Past Events
278
     *
279
     * @param integer $accountId
280
     * @param integer $page
281
     * @param integer $maxItems
282
     * @param string $order
283
     *
284
     * @return array
285
     *
286
     * @throws \LiveStream\Exceptions\LiveStreamException
287
     *
288
     * @see https://livestream.com/developers/docs/api/#get-past-events
289
     */
290
    public function getPastEvents(
291
        int $accountId,
292
        int $page = 1,
293
        int $maxItems = 10,
294
        string $order = 'desc'
295
    ): array {
296
        return $this->getEvents('past', $accountId, $page, $maxItems, $order);
297
    }
298
299
    /**
300
     * Get Private Events
301
     *
302
     * @param integer $accountId
303
     * @param integer $page
304
     * @param integer $maxItems
305
     * @param string $order
306
     *
307
     * @return array
308
     *
309
     * @throws \LiveStream\Exceptions\LiveStreamException
310
     *
311
     * @see https://livestream.com/developers/docs/api/#get-private-events
312
     */
313
    public function getPrivateEvents(
314
        int $accountId,
315
        int $page = 1,
316
        int $maxItems = 10,
317
        string $order = 'asc'
318
    ): array {
319
        return $this->getEvents('private', $accountId, $page, $maxItems, $order);
320
    }
321
322
    /**
323
     * Get Upcoming Events
324
     *
325
     * @param integer $accountId
326
     * @param integer $page
327
     * @param integer $maxItems
328
     * @param string $order
329
     *
330
     * @return array
331
     *
332
     * @throws \LiveStream\Exceptions\LiveStreamException
333
     *
334
     * @see https://livestream.com/developers/docs/api/#get-upcoming-events
335
     */
336
    public function geUpcomingEvents(
337
        int $accountId,
338
        int $page = 1,
339
        int $maxItems = 10,
340
        string $order = 'asc'
341
    ): array {
342
        return $this->getEvents('upcoming', $accountId, $page, $maxItems, $order);
343
    }
344
345
    /**
346
     * Get RTMP Key.
347
     *
348
     * @param  integer $accountId
349
     * @param  integer $eventId
350
     *
351
     * @return \LiveStream\Resources\RTMPKey|null
352
     */
353
    public function getRtmpKey(
354
        int $accountId,
355
        int $eventId,
356
        bool $notifyFollowers = false,
357
        bool $publishVideo = false,
358
        bool $saveVideo = false
359
    ): ?RTMPKey {
360
361
        $response = $this->request("accounts/$accountId/events/$eventId/rtmp", 'get', null, [
362
            'notifyFollowers' => $notifyFollowers,
363
            'publishVideo'    => $publishVideo,
364
            'saveVideo'       => $saveVideo
365
        ]);
366
367
        if ($response === null) return null;
368
369
        return RTMPKey::fromObject(json_decode($response));
370
    }
371
372
    /**
373
     * Reset RTMPKey
374
     *
375
     * @param  integer $accountId
376
     * @param  integer $eventId
377
     * @return \LiveStream\Resources\RTMPKey|null
378
     */
379
    public function resetRtmpKey(int $accountId, int $eventId): ?RTMPKey
380
    {
381
        $response = $this->request("accounts/$accountId/events/$eventId/rtmp", 'put');
382
383
        if ($response === null) return null;
384
385
        return RTMPKey::fromObject(json_decode($response));
386
    }
387
388
    /**
389
     * Gets current live Video for an Event, if one exists.
390
     *
391
     * @param int $accountId
392
     * @param int $eventId
393
     * @param ?int $offsetPostId
394
     * @param ?int $older
395
     * @param ?int $newer
396
     *
397
     * @return \LiveStream\Resources\Video|null
398
     *
399
     * @see https://livestream.com/developers/docs/api/#get-videos
400
     *
401
     * @throws \LiveStream\Exceptions\LiveStreamException
402
     */
403
    public function getLiveVideo(
404
      int $accountId,
405
      int $eventId,
406
      ?int $offsetPostId = null,
407
      ?int $older = null,
408
      ?int $newer = null
409
    ): ?Video {
410
        $video = null;
411
412
        $response = $this->request("accounts/$accountId/events/$eventId/videos", 'get', null, [
413
          'offsetPostId' => $offsetPostId,
414
          'older'        => $older,
415
          'newer'        => $newer
416
        ]);
417
418
        if ($response === null) return null;
419
420
        $response_data = json_decode($response);
421
422
        if ($response_data->live) {
423
            /** @var \LiveStream\Resources\Video $video */
424
            $video = Video::fromObject($response_data->live);
425
        }
426
427
        return $video;
428
    }
429
430
    /**
431
     * Gets URL of a Video's HLS stream (m3u8) file with access token.
432
     *
433
     * Requires a secure access token. Generates the URL using the "playback"
434
     * scope.
435
     *
436
     * @param \LiveStream\Resources\Video $video
437
     *
438
     * @return string|null
439
     */
440
    public function getVideoHlsFileUrl(Video $video): ?string {
441
        if (!$video->m3u8) return null;
442
443
        $token = $this->generateToken('playback');
444
        $query = [
445
          'clientId' => $this->clientId,
446
          'timestamp' => $token['timestamp'],
447
          'token' => $token['token']
448
        ];
449
        return $video->m3u8 . '?' . http_build_query($query);
450
    }
451
452
    /**
453
     * Gets contents of a Video's HLS stream (m3u8) file.
454
     *
455
     * Requires a secure access token.
456
     *
457
     * @param \LiveStream\Resources\Video $video
458
     *
459
     * @return string|null
460
     * @throws \LiveStream\Exceptions\LiveStreamException
461
     */
462
    public function getVideoHlsFileContents(Video $video): ?string {
463
        if (!$video->m3u8) return null;
464
465
        $endpoint = substr($video->m3u8, strlen(self::BASE_URL));
466
        return $this->request($endpoint);
467
    }
468
469
    /**
470
     * Refreshes a secure access token if invalid (5 minute life time).
471
     *
472
     * @see https://github.com/Livestream/livestream-api-samples/tree/master/php/secure-token-auth-sample
473
     */
474
    private function refreshToken(): void
475
    {
476
        $now = round(microtime(true) * 1000);
477
        if (!$this->tokenTimestamp || round(($now - $this->tokenTimestamp)/1000) > 300) {
478
            $token = $this->generateToken();
479
            $this->tokenTimestamp = $token['timestamp'];
480
            $this->token = $token['token'];
481
        }
482
    }
483
484
    /**
485
     * Generates a new token with optional scope override.
486
     *
487
     * @param string|null $scope
488
     *
489
     * @return array
490
     */
491
    private function generateToken(?string $scope = null): array {
492
        $scope = $scope ?? $this->scope;
493
        $timestamp = round(microtime(true) * 1000);
494
        $token = hash_hmac('md5', "{$this->apiKey}:{$scope}:{$timestamp}", $this->apiKey);
495
        return [
496
            'timestamp' => (int) $timestamp,
497
            'scope' => $scope,
498
            'token' => $token,
499
        ];
500
    }
501
502
    /**
503
     * CURL Request
504
     *
505
     * @param string $endpoint
506
     * @param string $verb
507
     * @param \LiveStream\Interfaces\Resource|null $body
508
     * @param array|null $query
509
     * @param array|null $multipartFormData
510
     *
511
     * @return string|null
512
     *
513
     * @throws \LiveStream\Exceptions\LiveStreamException
514
     */
515
    private function request(
516
        string $endpoint,
517
        string $verb = 'get',
518
        ?Resource $body = null,
519
        ?array $query = null,
520
        ?array $multipartFormData = null
521
    ): ?string {
522
        $ch = curl_init();
523
524
        if (!$ch) throw new Exception("Could not initialize CURL.");
0 ignored issues
show
introduced by
$ch is of type false|resource, thus it always evaluated to false.
Loading history...
525
526
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
527
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
528
529
        if ($this->token && $this->clientId && $this->tokenTimestamp) {
530
            $this->refreshToken();
531
            $query['timestamp'] = $this->tokenTimestamp;
532
            $query['clientId'] = $this->clientId;
533
            $query['token'] = $this->token;
534
        }
535
        else {
536
            curl_setopt($ch, CURLOPT_USERPWD, $this->apiKey . ':');
537
        }
538
539
        curl_setopt(
540
            $ch,
541
            CURLOPT_URL,
542
            $this->get_base_url() . $endpoint . ($query ? '?' . http_build_query($query) : '')
543
        );
544
545
        if ($verb != 'get') {
546
            if ($verb == 'post') curl_setopt($ch, CURLOPT_POST, true);
547
            if ($verb == 'put') curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
548
            if ($verb == 'delete') curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
549
            if ($body) {
550
                curl_setopt($ch, CURLOPT_HTTPHEADER, [
551
                    'Content-Type: ' . $body->getContentType()
552
                ]);
553
                curl_setopt($ch, CURLOPT_POSTFIELDS, $body->getResourceBody());
554
            } elseif ($multipartFormData) {
555
                curl_setopt($ch, CURLOPT_HTTPHEADER, [
556
                    'Content-Type: multipart/form-data'
557
                ]);
558
                curl_setopt($ch, CURLOPT_POSTFIELDS, $multipartFormData);
559
            }
560
        }
561
562
        $response = curl_exec($ch);
563
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
564
565
        curl_close($ch);
566
567
        if ($code == 200 || $code == 201) return $response;
568
569
        if ($code == 404) return null;
570
571
        if ($code <= 199) throw new Exception("A CURL erorr with code '$code', has occurred.");
572
573
        if ($code == 403) throw new LiveStreamException(self::ERROR_CODES[$code] . ' ' . json_decode($response)->message);
574
575
        throw new LiveStreamException(self::ERROR_CODES[$code]);
576
    }
577
578
    /**
579
     * Get Base URL.
580
     *
581
     * @return string
582
     */
583
    private function get_base_url(): string
584
    {
585
        return getenv('LIB_ENV') == 'testing' ? 'http://127.0.0.1:3067/' : self::BASE_URL;
586
    }
587
}
588