From a1918d15125f6c45ff5ae8efd970838b0940559f Mon Sep 17 00:00:00 2001 From: shimon Date: Tue, 21 Feb 2023 11:21:47 +0200 Subject: [PATCH] video tests --- Dockerfile | 3 +- app/config/videos-profiles.php | 27 ++ app/controllers/api/videos.php | 3 - app/init.php | 2 +- app/workers/transcoding.php | 6 +- composer.json | 2 +- composer.lock | 24 +- phpunit.xml | 1 + tests/e2e/Scopes/VideoCustom.php | 7 +- .../Videos/VideosCustomClientTest.php | 293 ++++++++++++++++ ...verTest.php => VideosCustomServerTest.php} | 327 +++++++++++------- 11 files changed, 558 insertions(+), 137 deletions(-) create mode 100644 tests/e2e/Services/Videos/VideosCustomClientTest.php rename tests/e2e/Services/Videos/{VideoCustomServerTest.php => VideosCustomServerTest.php} (81%) diff --git a/Dockerfile b/Dockerfile index 2f6cb455ee..0b70fb7b19 100755 --- a/Dockerfile +++ b/Dockerfile @@ -126,9 +126,8 @@ ENV _APP_SERVER=swoole \ RUN \ apk update \ - && apk add --no-cache --virtual .deps \ + && apk add --no-cache\ ffmpeg \ - && apk del .deps \ && rm -rf /var/cache/apk/* RUN \ diff --git a/app/config/videos-profiles.php b/app/config/videos-profiles.php index e68b3dbbb7..f1b0bcff0a 100644 --- a/app/config/videos-profiles.php +++ b/app/config/videos-profiles.php @@ -1,6 +1,32 @@ '360p', + 'videoBitrate' => 890, //video bitrate in Kbps + 'audioBitrate' => 64, //audio bitrate in Kbps + 'width' => 640, //width resolution in px + 'height' => 360, //height resolution in px + 'output' => 'hls' + ], + [ + 'name' => '576p', + 'videoBitrate' => 2538, + 'audioBitrate' => 128, + 'width' => 1024, + 'height' => 576, + 'output' => 'hls' + ], + [ + 'name' => '720p', + 'videoBitrate' => 3551, + 'audioBitrate' => 128, + 'width' => 1280, + 'height' => 720, + 'output' => 'hls' + ], + [ 'name' => '360p', @@ -27,4 +53,5 @@ return [ 'output' => 'dash' ], + ]; diff --git a/app/controllers/api/videos.php b/app/controllers/api/videos.php index 24a0bf9ec1..6a36ad862e 100644 --- a/app/controllers/api/videos.php +++ b/app/controllers/api/videos.php @@ -1059,9 +1059,6 @@ App::get('/v1/videos/profiles') ->action(function (Response $response, Database $dbForProject) { $profiles = Authorization::skip(fn () => $dbForProject->find('videos_profiles')); - if (empty($profiles)) { - throw new Exception(Exception::VIDEO_PROFILE_NOT_FOUND); - } $response->dynamic(new Document([ 'total' => $dbForProject->count('videos_profiles', [], APP_LIMIT_COUNT), diff --git a/app/init.php b/app/init.php index b895d92aa8..2b1ed21d46 100644 --- a/app/init.php +++ b/app/init.php @@ -889,7 +889,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) { /** @var Utopia\Database\Document $console */ $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console')); - $projectId = '63f22d4b78b296b2d47d'; + //$projectId = '63f22d4b78b296b2d47d'; if ($projectId === 'console') { return $console; } diff --git a/app/workers/transcoding.php b/app/workers/transcoding.php index d6c94a31d9..fdcec0f7c7 100644 --- a/app/workers/transcoding.php +++ b/app/workers/transcoding.php @@ -512,9 +512,9 @@ class TranscodingV1 extends Worker $general = $metadata['stream']['resolutions'][0] ?? []; $parts = explode('X', $general['dimension']); $info['width'] = $parts[0] ?? $representation->getWidth(); - $info['height'] = isset($parts[1]) ?? $representation->getHeight(); - $info['videoBitrate'] = isset($general['video_kilo_bitrate']) ?? '0'; - $info['audioBitrate'] = isset($general['audio_kilo_bitrate']) ?? '0'; + $info['height'] = $parts[1] ?? $representation->getHeight(); + $info['videoBitrate'] = $general['video_kilo_bitrate'] ?? '0'; + $info['audioBitrate'] = $general['audio_kilo_bitrate'] ?? '0'; foreach ($metadata['video']['streams'] ?? [] as $streams) { if ($streams['codec_type'] === 'video') { diff --git a/composer.json b/composer.json index e1d1231924..63727cc7c9 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "utopia-php/messaging": "0.1.*", "utopia-php/orchestration": "0.6.*", "utopia-php/registry": "0.5.*", - "utopia-php/storage": "0.13.*", + "utopia-php/storage": "dev-feat-transfer as 0.13.2", "utopia-php/swoole": "0.5.*", "utopia-php/websocket": "0.1.*", "resque/php-resque": "1.3.6", diff --git a/composer.lock b/composer.lock index f478c3ffc4..c3bd4d8a2a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1577b45260165bbdf1367d5a24e4e79f", + "content-hash": "b636b84edd26b787ae58b3dbdc752da4", "packages": [ { "name": "adhocore/jwt", @@ -3798,16 +3798,16 @@ }, { "name": "utopia-php/storage", - "version": "0.13.2", + "version": "dev-feat-transfer", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "ad1c00f24ca56e73888acc2af3deee4919b1194b" + "reference": "00b26ef3d04b04f2df64a64636f6b81f8ae1a166" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/ad1c00f24ca56e73888acc2af3deee4919b1194b", - "reference": "ad1c00f24ca56e73888acc2af3deee4919b1194b", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/00b26ef3d04b04f2df64a64636f6b81f8ae1a166", + "reference": "00b26ef3d04b04f2df64a64636f6b81f8ae1a166", "shasum": "" }, "require": { @@ -3847,9 +3847,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.13.2" + "source": "https://github.com/utopia-php/storage/tree/feat-transfer" }, - "time": "2022-12-20T11:11:35+00:00" + "time": "2022-11-02T07:42:34+00:00" }, { "name": "utopia-php/swoole", @@ -6506,9 +6506,17 @@ "time": "2023-02-08T07:49:20+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/storage", + "version": "dev-feat-transfer", + "alias": "0.13.2", + "alias_normalized": "0.13.2.0" + } + ], "minimum-stability": "stable", "stability-flags": { + "utopia-php/storage": 20, "aminyazdanpanah/php-ffmpeg-video-streaming": 20, "appwrite/sdk-generator": 20 }, diff --git a/phpunit.xml b/phpunit.xml index 5b4bcdb99c..5c5c7f593c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -31,6 +31,7 @@ ./tests/e2e/Services/Teams ./tests/e2e/Services/Users ./tests/e2e/Services/Webhooks + ./tests/e2e/Services/Videos ./tests/e2e/Services/Functions/FunctionsBase.php ./tests/e2e/Services/Functions/FunctionsCustomServerTest.php ./tests/e2e/Services/Functions/FunctionsCustomClientTest.php diff --git a/tests/e2e/Scopes/VideoCustom.php b/tests/e2e/Scopes/VideoCustom.php index a3275d0ae5..a6bd22a4db 100644 --- a/tests/e2e/Scopes/VideoCustom.php +++ b/tests/e2e/Scopes/VideoCustom.php @@ -4,9 +4,10 @@ namespace Tests\E2E\Scopes; use Tests\E2E\Client; use Utopia\Database\DateTime; -use Utopia\Database\ID; -use Utopia\Database\Permission; -use Utopia\Database\Role; +use Utopia\Database\Helpers\ID; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\validator\DateTimeValidator; +use Utopia\Database\Helpers\Role; trait VideoCustom { diff --git a/tests/e2e/Services/Videos/VideosCustomClientTest.php b/tests/e2e/Services/Videos/VideosCustomClientTest.php new file mode 100644 index 0000000000..c577367989 --- /dev/null +++ b/tests/e2e/Services/Videos/VideosCustomClientTest.php @@ -0,0 +1,293 @@ +client->call(Client::METHOD_GET, '/videos/profiles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals(6, $response['body']['total']); + + $profiles = $response['body']['profiles']; + foreach ($profiles as $profile) { + $response = $this->client->call(Client::METHOD_DELETE, '/videos/profiles/' . $profile['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(204, $response['headers']['status-code']); + } + + $response = $this->client->call(Client::METHOD_GET, '/videos/profiles/' . $profile['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals('Video profile not found.', $response['body']['message']); + } + + public function testTranscodeWithSubs() + { + /** + * Create video + */ + $response = $this->client->call(Client::METHOD_POST, '/videos', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => $this->getBucket()['$id'], + 'fileId' => $this->getVideo()['$id'] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + + $videoId = $response['body']['$id']; + + /** + * Create subtitles + */ + $response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/subtitles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => $this->getBucket()['$id'], + 'fileId' => $this->getSubtitle()['$id'], + 'name' => 'English', + 'code' => 'Eng', + 'default' => true, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + + $response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/subtitles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => $this->getBucket()['$id'], + 'fileId' => $this->getSubtitle()['$id'], + 'name' => 'Italian', + 'code' => 'It', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + + /** + * Create profiles + */ + $response = $this->client->call(Client::METHOD_POST, '/videos/profiles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'name' => 'Profile A', + 'videoBitrate' => 770, + 'audioBitrate' => 64, + 'width' => 600, + 'height' => 400, + 'output' => 'hls', + ]); + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/videos/profiles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'name' => 'Profile B', + 'videoBitrate' => 570, + 'audioBitrate' => 64, + 'width' => 300, + 'height' => 200, + 'output' => 'dash', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(2, $response['body']['total']); + $this->assertNotEmpty($response['body']['profiles']); + $profiles = $response['body']['profiles']; + + foreach ($profiles as $profile) { + $response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/rendition', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'profileId' => $profile['$id'], + ]); + + $this->assertEquals(204, $response['headers']['status-code']); + } + sleep(30); + $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/outputs/hls', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match); + $this->assertEquals(3, count($match[0])); + $subtitleUri = $match[0][1]; + $renditionUri = $match[0][2]; + + $response = $this->client->call(Client::METHOD_GET, $renditionUri, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match); + $this->assertEquals(10, count($match[0])); + + $segmentUri = $match[0][0]; + $response = $this->client->call(Client::METHOD_GET, $segmentUri, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThan(0, strlen($response['body'])); + + $response = $this->client->call(Client::METHOD_GET, $subtitleUri, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match); + $segmentUri = $match[0][0]; + + $response = $this->client->call(Client::METHOD_GET, $segmentUri, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1508, strlen($response['body'])); + + $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/outputs/dash', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + + $xml = simplexml_load_string($response['body']); + + $this->assertEquals("PT1M32.7S", $xml->attributes()->mediaPresentationDuration); + $this->assertEquals("PT10.0S", $xml->attributes()->maxSegmentDuration); + $this->assertEquals("PT20.0S", $xml->attributes()->minBufferTime); + $subsCount = 0; + $isVideo = false; + $isAudio = false; + $subs[] = ['id' => '2', 'lang' => 'Eng',]; + $subs[] = ['id' => '3', 'lang' => 'It',]; + foreach ($xml->Period->AdaptationSet as $adaptation) { + if ((string)$adaptation['contentType'] === 'video') { + $isVideo = true; + $this->assertEquals("50/1", $adaptation['frameRate']); + $this->assertEquals("300", $adaptation['maxWidth']); + $this->assertEquals("30:17", $adaptation['par']); + $this->assertEquals("und", $adaptation['lang']); + foreach ($adaptation->Representation as $representation) { + $this->assertEquals("video/mp4", $representation['mimeType']); + $this->assertEquals("avc1.640015", $representation['codecs']); + $this->assertEquals("300", $representation['width']); + $this->assertEquals("200", $representation['height']); + $this->assertEquals("20:17", $representation['sar']); + $this->assertEquals(10, $representation->SegmentList->SegmentURL->count()); + $videoSegmentBaseUrl = (string)$representation->BaseURL; + $videoSegmentInitialization = (string)$representation->SegmentList->Initialization['sourceURL']; + $videoSegmentId = (string)$representation->SegmentList->SegmentURL['media']; + + $response = $this->client->call(Client::METHOD_GET, $videoSegmentBaseUrl . $videoSegmentInitialization, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThan(0, strlen($response['body'])); + + $response = $this->client->call(Client::METHOD_GET, $videoSegmentBaseUrl . $videoSegmentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThan(0, strlen($response['body'])); + } + } elseif ((string)$adaptation['contentType'] === 'audio') { + $isAudio = true; + foreach ($adaptation->Representation as $representation) { + $this->assertEquals("audio/mp4", $representation['mimeType']); + $this->assertEquals("mp4a.40.2", $representation['codecs']); + $this->assertEquals(10, $representation->SegmentList->SegmentURL->count()); + $audioSegmentBaseUrl = (string)$representation->BaseURL; + $audioSegmentInitialization = (string)$representation->SegmentList->Initialization['sourceURL']; + $audioSegmentId = (string)$representation->SegmentList->SegmentURL['media']; + + $response = $this->client->call(Client::METHOD_GET, $audioSegmentBaseUrl . $audioSegmentInitialization, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThan(0, strlen($response['body'])); + + $response = $this->client->call(Client::METHOD_GET, $audioSegmentBaseUrl . $audioSegmentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThan(0, strlen($response['body'])); + } + } elseif ((string)$adaptation['mimeType'] === 'text/vtt') { + $this->assertEquals($subs[$subsCount]['id'], $adaptation['id']); + $this->assertEquals($subs[$subsCount]['lang'], $adaptation['lang']); + $subsCount++; + } + } + + $this->assertEquals($subsCount, 2); + $this->assertTrue($isVideo); + $this->assertTrue($isAudio); + } +} diff --git a/tests/e2e/Services/Videos/VideoCustomServerTest.php b/tests/e2e/Services/Videos/VideosCustomServerTest.php similarity index 81% rename from tests/e2e/Services/Videos/VideoCustomServerTest.php rename to tests/e2e/Services/Videos/VideosCustomServerTest.php index f17117d016..3acfd49db0 100644 --- a/tests/e2e/Services/Videos/VideoCustomServerTest.php +++ b/tests/e2e/Services/Videos/VideosCustomServerTest.php @@ -7,15 +7,47 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\VideoCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -use Utopia\Database\Permission; -use Utopia\Database\Role; -class VideoCustomServerTest extends Scope +class VideosCustomServerTest extends Scope { use ProjectCustom; use VideoCustom; use SideServer; + public function testDeleteAllProfiles() + { + + $response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals(6, $response['body']['total']); + + $profiles = $response['body']['profiles']; + foreach ($profiles as $profile) { + $response = $this->client->call(Client::METHOD_DELETE, '/videos/profiles/' . $profile['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(204, $response['headers']['status-code']); + } + + $response = $this->client->call(Client::METHOD_GET, '/videos/profiles/' . $profile['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals('Video profile not found.', $response['body']['message']); + } + public function testCreateProfile(): string { $response = $this->client->call(Client::METHOD_POST, '/videos/profiles', [ @@ -28,21 +60,20 @@ class VideoCustomServerTest extends Scope 'audioBitrate' => 120, 'width' => 600, 'height' => 400, - 'protocol' => 'hls', + 'output' => 'hls', ]); - $profileId = $response['body']['$id']; - $this->assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals(201, $response['headers']['status-code']); - return $profileId; + return $response['body']['$id']; } /** * @depends testCreateProfile */ - public function testUpdateProfile(string $profileId) + public function testUpdateProfile(string $profileId): string { $response = $this->client->call(Client::METHOD_PATCH, '/videos/profiles/' . $profileId, [ @@ -55,16 +86,18 @@ class VideoCustomServerTest extends Scope 'audioBitrate' => 120, 'width' => 300, 'height' => 400, - 'protocol' => 'dash', + 'output' => 'dash', ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); + + return $response['body']['$id']; } /** - * @depends testCreateProfile + * @depends testUpdateProfile */ public function testGetProfile(string $profileId): string { @@ -74,74 +107,21 @@ class VideoCustomServerTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'], ]); - $profileId = $response['body']['$id']; $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); $this->assertEquals('My updated test profile', $response['body']['name']); $this->assertEquals(300, $response['body']['width']); $this->assertEquals(400, $response['body']['height']); + $this->assertEquals('dash', $response['body']['output']); - return $profileId; + return $response['body']['$id']; } + /** * @depends testGetProfile */ - public function testGetVideoWithUserPermissions(string $profileId) - { - - $source = __DIR__ . "/../../../resources/disk-a/video-srt.mp4"; - $totalSize = \filesize($source); - $chunkSize = 5 * 1024 * 1024; - $handle = @fopen($source, "rb"); - $fileId = 'unique()'; - $mimeType = mime_content_type($source); - $counter = 0; - $size = filesize($source); - $headers = [ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'] - ]; - $id = ''; - - while (!feof($handle)) { - $curlFile = new \CURLFile('data:' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'video-srt.mp4'); - $headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size) . '/' . $size; - - if (!empty($id)) { - $headers['x-appwrite-id'] = $id; - } - - $_file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $this->getBucket()['$id'] . '/files', array_merge($headers, $this->getHeaders()), [ - 'fileId' => $fileId, - 'file' => $curlFile, - 'fileSecurity' => true, - 'permissions' => [ - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), - ], - - ]); - $counter++; - $fileId = $_file['body']['$id']; - } - @fclose($handle); - - $response = $this->client->call(Client::METHOD_POST, '/videos', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'bucketId' => $this->getBucket()['$id'], - 'fileId' => $fileId - ]); - - } - - /** - * @depends testCreateProfile - */ public function testDeleteProfile(string $profileId) { @@ -171,46 +151,46 @@ class VideoCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertEquals(3, $response['body']['total']); + $this->assertEquals(0, $response['body']['total']); } - public function testDeleteAllProfiles() + public function testCreateProfiles(): void { - - $response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [ + $response = $this->client->call(Client::METHOD_POST, '/videos/profiles', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'name' => 'My test profile', + 'videoBitrate' => 570, + 'audioBitrate' => 120, + 'width' => 600, + 'height' => 400, + 'output' => 'hls', ]); - $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertEquals(3, $response['body']['total']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals(201, $response['headers']['status-code']); - $profiles = $response['body']['profiles']; - foreach ($profiles as $profile) { - $response = $this->client->call(Client::METHOD_DELETE, '/videos/profiles/' . $profile['$id'], [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ]); - $this->assertEquals(204, $response['headers']['status-code']); - } - - $response = $this->client->call(Client::METHOD_GET, '/videos/profiles/' . $profile['$id'], [ + $response = $this->client->call(Client::METHOD_POST, '/videos/profiles', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'name' => '576p', + 'videoBitrate' => 2538, + 'audioBitrate' => 128, + 'width' => 1024, + 'height' => 576, + 'output' => 'hls', ]); - $this->assertEquals(404, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertEquals('Video profile not found.', $response['body']['message']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals(201, $response['headers']['status-code']); } - /** - * @depends testDeleteAllProfiles - */ public function testCreateVideo(): string { $response = $this->client->call(Client::METHOD_POST, '/videos', [ @@ -375,6 +355,27 @@ class VideoCustomServerTest extends Scope */ public function testTranscodeWithSubs($videoId): string { + + $response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals(2, $response['body']['total']); + + $profiles = $response['body']['profiles']; + foreach ($profiles as $profile) { + $response = $this->client->call(Client::METHOD_DELETE, '/videos/profiles/' . $profile['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(204, $response['headers']['status-code']); + } + $response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/subtitles', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -431,7 +432,7 @@ class VideoCustomServerTest extends Scope 'audioBitrate' => 64, 'width' => 600, 'height' => 400, - 'protocol' => 'hls', + 'output' => 'hls', ]); $this->assertEquals(201, $response['headers']['status-code']); @@ -445,7 +446,7 @@ class VideoCustomServerTest extends Scope 'audioBitrate' => 64, 'width' => 300, 'height' => 200, - 'protocol' => 'dash', + 'output' => 'dash', ]); $this->assertEquals(201, $response['headers']['status-code']); @@ -534,7 +535,7 @@ class VideoCustomServerTest extends Scope /** * @depends testGetRenditions */ - public function testProtocolWithSubs($videoId): string + public function testOutputWithSubs($videoId): string { $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/outputs/hls', [ @@ -544,9 +545,9 @@ class VideoCustomServerTest extends Scope ]); $this->assertEquals(200, $response['headers']['status-code']); preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match); - $this->assertEquals(4, count($match[0])); + $this->assertEquals(3, count($match[0])); $subtitleUri = $match[0][1]; - $renditionUri = $match[0][3]; + $renditionUri = $match[0][2]; $response = $this->client->call(Client::METHOD_GET, $renditionUri, [ 'content-type' => 'application/json', @@ -555,7 +556,7 @@ class VideoCustomServerTest extends Scope ]); $this->assertEquals(200, $response['headers']['status-code']); preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match); - $this->assertEquals(12, count($match[0])); + $this->assertEquals(10, count($match[0])); $segmentUri = $match[0][0]; $response = $this->client->call(Client::METHOD_GET, $segmentUri, [ @@ -595,8 +596,8 @@ class VideoCustomServerTest extends Scope $xml = simplexml_load_string($response['body']); $this->assertEquals("PT1M32.7S", $xml->attributes()->mediaPresentationDuration); - $this->assertEquals("PT8.0S", $xml->attributes()->maxSegmentDuration); - $this->assertEquals("PT20.5S", $xml->attributes()->minBufferTime); + $this->assertEquals("PT10.0S", $xml->attributes()->maxSegmentDuration); + $this->assertEquals("PT20.0S", $xml->attributes()->minBufferTime); $subsCount = 0; $isVideo = false; $isAudio = false; @@ -605,13 +606,13 @@ class VideoCustomServerTest extends Scope foreach ($xml->Period->AdaptationSet as $adaptation) { if ((string)$adaptation['contentType'] === 'video') { $isVideo = true; - $this->assertEquals("24/1", $adaptation['frameRate']); + $this->assertEquals("50/1", $adaptation['frameRate']); $this->assertEquals("300", $adaptation['maxWidth']); $this->assertEquals("30:17", $adaptation['par']); $this->assertEquals("und", $adaptation['lang']); foreach ($adaptation->Representation as $representation) { $this->assertEquals("video/mp4", $representation['mimeType']); - $this->assertEquals("avc1.640014", $representation['codecs']); + $this->assertEquals("avc1.640015", $representation['codecs']); $this->assertEquals("300", $representation['width']); $this->assertEquals("200", $representation['height']); $this->assertEquals("20:17", $representation['sar']); @@ -643,7 +644,7 @@ class VideoCustomServerTest extends Scope foreach ($adaptation->Representation as $representation) { $this->assertEquals("audio/mp4", $representation['mimeType']); $this->assertEquals("mp4a.40.2", $representation['codecs']); - $this->assertEquals(12, $representation->SegmentList->SegmentURL->count()); + $this->assertEquals(10, $representation->SegmentList->SegmentURL->count()); $audioSegmentBaseUrl = (string)$representation->BaseURL; $audioSegmentInitialization = (string)$representation->SegmentList->Initialization['sourceURL']; $audioSegmentId = (string)$representation->SegmentList->SegmentURL['media']; @@ -682,7 +683,7 @@ class VideoCustomServerTest extends Scope /** - * @depends testProtocolWithSubs + * @depends testOutputWithSubs */ public function testDeleteVideo($videoId): string { @@ -701,7 +702,7 @@ class VideoCustomServerTest extends Scope /** * @depends testDeleteVideo */ - public function testProtocolWithSubsAgain($videoId): string + public function testOutputWithSubsAgain($videoId): string { $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/outputs/hls', [ @@ -769,19 +770,20 @@ class VideoCustomServerTest extends Scope /** * @depends testTranscodeWithoutSubs */ - public function testProtocolWithoutSubs($videoId) + public function testOutputWithoutSubs($videoId): string { - - sleep(50); + sleep(30); $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/outputs/hls', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]); + + $this->assertEquals(200, $response['headers']['status-code']); preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match); - $this->assertEquals(2, count($match[0])); + $this->assertEquals(1, count($match[0])); $renditionUri = $match[0][0]; $response = $this->client->call(Client::METHOD_GET, $renditionUri, [ @@ -791,7 +793,7 @@ class VideoCustomServerTest extends Scope ]); $this->assertEquals(200, $response['headers']['status-code']); preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match); - $this->assertEquals(12, count($match[0])); + $this->assertEquals(10, count($match[0])); $segmentUri = $match[0][0]; $response = $this->client->call(Client::METHOD_GET, $segmentUri, [ @@ -813,20 +815,20 @@ class VideoCustomServerTest extends Scope $xml = simplexml_load_string($response['body']); $this->assertEquals("PT1M32.7S", $xml->attributes()->mediaPresentationDuration); - $this->assertEquals("PT8.0S", $xml->attributes()->maxSegmentDuration); - $this->assertEquals("PT20.5S", $xml->attributes()->minBufferTime); + $this->assertEquals("PT10.0S", $xml->attributes()->maxSegmentDuration); + $this->assertEquals("PT20.0S", $xml->attributes()->minBufferTime); $isVideo = false; foreach ($xml->Period->AdaptationSet as $adaptation) { if ((string)$adaptation['contentType'] === 'video') { $isVideo = true; - $this->assertEquals("24/1", $adaptation['frameRate']); + $this->assertEquals("50/1", $adaptation['frameRate']); $this->assertEquals("300", $adaptation['maxWidth']); $this->assertEquals("30:17", $adaptation['par']); $this->assertEquals("und", $adaptation['lang']); foreach ($adaptation->Representation as $representation) { $this->assertEquals("video/mp4", $representation['mimeType']); - $this->assertEquals("avc1.640014", $representation['codecs']); + $this->assertEquals("avc1.640015", $representation['codecs']); $this->assertEquals("300", $representation['width']); $this->assertEquals("200", $representation['height']); $this->assertEquals("20:17", $representation['sar']); @@ -858,7 +860,7 @@ class VideoCustomServerTest extends Scope foreach ($adaptation->Representation as $representation) { $this->assertEquals("audio/mp4", $representation['mimeType']); $this->assertEquals("mp4a.40.2", $representation['codecs']); - $this->assertEquals(12, $representation->SegmentList->SegmentURL->count()); + $this->assertEquals(10, $representation->SegmentList->SegmentURL->count()); $audioSegmentBaseUrl = (string)$representation->BaseURL; $audioSegmentInitialization = (string)$representation->SegmentList->Initialization['sourceURL']; $audioSegmentId = (string)$representation->SegmentList->SegmentURL['media']; @@ -886,11 +888,46 @@ class VideoCustomServerTest extends Scope $this->assertTrue($isVideo); $this->assertTrue($isAudio); + + return $videoId; } - - public function testGetVideos() + /** + * @depends testTranscodeWithoutSubs + */ + public function testDeleteRendition($videoId) { + /** + * Delete one rendition + */ + $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/renditions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertNotEmpty($response['body']['renditions']); + $this->assertEquals(2, $response['body']['total']); + + $response = $this->client->call(Client::METHOD_DELETE, '/videos/' . $videoId . '/renditions/' . $response['body']['renditions'][0]['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(204, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/renditions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertNotEmpty($response['body']['renditions']); + $this->assertEquals(1, $response['body']['total']); + + /** + * Get videos + */ $response = $this->client->call(Client::METHOD_GET, '/videos', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -899,9 +936,67 @@ class VideoCustomServerTest extends Scope ], [ 'queries' => [ 'limit(10)' ] ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals(1, count($response['body']['videos'])); + + /** + * Delete the video + */ + $response = $this->client->call(Client::METHOD_DELETE, '/videos/' . $videoId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(204, $response['headers']['status-code']); + + /** + * Get videos again + */ + $response = $this->client->call(Client::METHOD_GET, '/videos', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + + ], [ + 'queries' => [ 'limit(10)' ] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(0, $response['body']['total']); + $this->assertEquals(0, count($response['body']['videos'])); + + /** + * Delete all profiles (cleanup) + */ + $response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertGreaterThan(0, $response['body']['total']); - $this->assertGreaterThan(0, count($response['body']['videos'])); + $this->assertNotEmpty($response['body']); + $this->assertEquals(2, $response['body']['total']); + + $profiles = $response['body']['profiles']; + foreach ($profiles as $profile) { + $response = $this->client->call(Client::METHOD_DELETE, '/videos/profiles/' . $profile['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(204, $response['headers']['status-code']); + } + + $response = $this->client->call(Client::METHOD_GET, '/videos/profiles/' . $profile['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertEquals('Video profile not found.', $response['body']['message']); } }