diff --git a/app/config/collections.php b/app/config/collections.php index b9688f6903..50b6360689 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -2872,6 +2872,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'size', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'duration', 'type' => Database::VAR_STRING, @@ -2928,7 +2939,7 @@ $collections = [ 'filters' => [], ], [ - '$id' => 'videoFrameRate', + '$id' => 'videoFramerate', 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -2961,7 +2972,7 @@ $collections = [ 'filters' => [], ], [ - '$id' => 'audioSampleRate', + '$id' => 'audioSamplerate', 'type' => Database::VAR_INTEGER, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -2988,18 +2999,7 @@ $collections = [ '$name' => 'Video_renditions', 'attributes' => [ [ - '$id' => 'bucketId', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'fileId', + '$id' => 'videoId', 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -3024,7 +3024,7 @@ $collections = [ '$id' => 'name', 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 2048, + 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, 'default' => null, @@ -3053,6 +3053,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'path', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'duration', 'type' => Database::VAR_STRING, @@ -3066,10 +3077,10 @@ $collections = [ ], [ '$id' => 'width', - 'type' => Database::VAR_INTEGER, + 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 0, - 'signed' => false, + 'size' => Database::LENGTH_KEY, + 'signed' => true, 'required' => false, 'default' => null, 'array' => false, @@ -3077,10 +3088,10 @@ $collections = [ ], [ '$id' => 'height', - 'type' => Database::VAR_INTEGER, + 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 0, - 'signed' => false, + 'size' => Database::LENGTH_KEY, + 'signed' => true, 'required' => false, 'default' => null, 'array' => false, @@ -3099,17 +3110,17 @@ $collections = [ ], [ '$id' => 'videoBitrate', - 'type' => Database::VAR_INTEGER, + 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, - 'default' => 0, + 'default' => null, 'array' => false, 'filters' => [], ], [ - '$id' => 'videoFrameRate', + '$id' => 'videoFramerate', 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -3132,23 +3143,23 @@ $collections = [ ], [ '$id' => 'audioBitrate', - 'type' => Database::VAR_INTEGER, + 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, - 'default' => 0, + 'default' => null, 'array' => false, 'filters' => [], ], [ - '$id' => 'audioSampleRate', - 'type' => Database::VAR_INTEGER, + '$id' => 'audioSamplerate', + 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, - 'default' => 0, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -3156,7 +3167,7 @@ $collections = [ '$id' => 'metadata', 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 16384, + 'size' => 2048, 'signed' => true, 'required' => false, 'default' => null, @@ -3167,7 +3178,7 @@ $collections = [ '$id' => 'status', 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 100, + 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, 'default' => null, @@ -3178,7 +3189,7 @@ $collections = [ '$id' => 'progress', 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 4, + 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, 'default' => null, @@ -3189,7 +3200,7 @@ $collections = [ '$id' => 'stream', 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 255, + 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => true, 'default' => null, @@ -3201,7 +3212,7 @@ $collections = [ [ '$id' => '_key_bucket_file_stream', 'type' => Database::INDEX_KEY, - 'attributes' => ['bucketId', 'fileId', 'stream'], + 'attributes' => ['videoId', 'stream'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 02bfbdeb8a..f3081445df 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -160,8 +160,9 @@ App::post('/v1/projects') 'name' => $profile['name'], 'videoBitrate' => $profile['videoBitrate'], 'audioBitrate' => $profile['audioBitrate'], - 'width' => $profile['width'], - 'height' => $profile['height'] + 'width' => $profile['width'], + 'height' => $profile['height'], + 'stream' => 'hls' ])); }); } diff --git a/app/controllers/api/video.php b/app/controllers/api/video.php index c677136902..ef3d816ea3 100644 --- a/app/controllers/api/video.php +++ b/app/controllers/api/video.php @@ -44,9 +44,31 @@ use Utopia\Validator\WhiteList; use Utopia\Swoole\Request; use Streaming\Representation; +App::get('/v1/video/profiles') + ->alias('/v1/video/video/profiles', []) + ->desc('Get all video profiles') + ->groups(['api', 'storage']) + ->label('scope', 'files.read') + ->inject('response') + ->inject('dbForProject') + ->action(function (Response $response, Database $dbForProject) { + + $profiles = Authorization::skip(fn () => $dbForProject->find('video_profiles', [], 12, 0, [], ['ASC'])); + + if(empty($profiles)) { + throw new Exception('Video profiles where not found', 404, Exception::PROFILES_NOT_FOUND); + } + + $response->dynamic(new Document([ + 'total' => $dbForProject->count('video_profiles', [], APP_LIMIT_COUNT), + 'profiles' => $profiles, + ]), Response::MODEL_VIDEO_PROFILE_LIST); + }); + + App::post('/v1/video/buckets/:bucketId/files/:fileId') ->alias('/v1/video/files', ['bucketId' => 'default']) - ->desc('Start transcoding video') + ->desc('Create video') ->groups(['api', 'storage']) ->label('scope', 'files.write') // ->label('event', 'buckets.[bucketId].files.[fileId].create') @@ -106,36 +128,121 @@ App::post('/v1/video/buckets/:bucketId/files/:fileId') } } - $profiles = Authorization::skip(fn () => $dbForProject->find('video_profiles', [], 12, 0, [], ['ASC'])); - - if ($profiles->empty()) { - throw new Exception('No video profiles found', 400, Exception::PROFILES_NOT_FOUND); + if ($bucket->getAttribute('permission') === 'bucket') { + // skip authorization + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + } else { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } - - $queries = [ - new Query('bucketId', Query::TYPE_EQUAL, [$bucketId]), - new Query('fileId', Query::TYPE_EQUAL, [$fileId]), - ]; - - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [$queries])); - if($video->empty()) { - $video = Authorization::skip(function () use ($dbForProject) { - return $this->database->createDocument('videos', new Document([ - 'bucketId' => $this->args['bucketId'], - 'fileId' => $this->args['fileId'], + try { + $video = Authorization::skip(function () use ($dbForProject, $bucket, $file) { + return $dbForProject->createDocument('videos', new Document([ + 'bucketId' => $bucket->getId(), + 'fileId' => $file->getId(), + 'size' => $file->getAttribute('sizeOriginal'), ])); }); + } catch (StructureException $exception) { + throw new Exception($exception->getMessage(), 400, Exception::DOCUMENT_INVALID_STRUCTURE); + } + + $response->dynamic(new Document([ + '$id' => $video->getId(), + 'fileId' => $video['fileId'], + 'bucketId' => $video['bucketId'], + 'size' => $video['size'], + ]), Response::MODEL_VIDEO); + }); + + +App::post('/v1/video/:videoId/rendition/:profileId') + ->alias('/v1/video/files', ['bucketId' => 'default']) + ->desc('Start transcoding video rendition') + ->groups(['api', 'storage']) + ->label('scope', 'files.write') +// ->label('event', 'buckets.[bucketId].files.[fileId].create') + ->param('videoId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).') + ->param('profileId', '', new CustomId(), 'File ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('user') + ->inject('audits') + ->inject('usage') + ->inject('events') + ->inject('mode') + ->inject('deviceFiles') + ->inject('deviceLocal') + ->action(action: function (string $videoId, string $profileId, ?array $read, ?array $write, Request $request, Response $response, Database $dbForProject, $project, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) { + /** @var Utopia\Database\Document $project */ + /** @var Utopia\Database\Document $user */ + + $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); + + if($video->isEmpty()){ + throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND); + } + + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $video['bucketId'])); + + if ( + $bucket->isEmpty() + || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN) + ) { + throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND); + } + + // Check bucket permissions when enforced + $permissionBucket = $bucket->getAttribute('permission') === 'bucket'; + if ($permissionBucket) { + $validator = new Authorization('write'); + if (!$validator->isValid($bucket->getWrite())) { + throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); + } + } + + $read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user + $write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? []; + + // Users can only add their roles to files, API keys and Admin users can add any + $roles = Authorization::getRoles(); + + if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { + foreach ($read as $role) { + if (!Authorization::isRole($role)) { + throw new Exception('Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + } + } + foreach ($write as $role) { + if (!Authorization::isRole($role)) { + throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + } + } + } + + if ($bucket->getAttribute('permission') === 'bucket') { + // skip authorization + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId'])); + } else { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId']); + } + + $profile = Authorization::skip(fn() => $dbForProject->findOne('video_profiles', [new Query('_uid', Query::TYPE_EQUAL, [$profileId])])); + + if(!$profile){ + throw new Exception('Video profile not found', 400, Exception::PROFILES_NOT_FOUND); } $transcoder = new Transcoding(); - foreach ($profiles as $profile) { - $transcoder - ->setUser($user) - ->setProject($project) - ->setVideoId($video->getId()) - ->setProfileId($profile->getId()) - ->trigger(); - } + $transcoder + ->setUser($user) + ->setProject($project) + ->setVideoId($video->getId()) + ->setProfileId($profile->getId()) + ->trigger(); $response->json(['result' => 'ok']); }); diff --git a/app/workers/transcoding.php b/app/workers/transcoding.php index 83a13198cf..2fe768f119 100644 --- a/app/workers/transcoding.php +++ b/app/workers/transcoding.php @@ -23,6 +23,15 @@ Console::success(APP_NAME . ' transcoding worker v1 has started'); class TranscodingV1 extends Worker { + /** + * Rendition Status + */ + const STATUS_TRANSCODE_START = 'started'; + const STATUS_TRANSCODE_END = 'ended'; + const STATUS_UPLOADING = 'uploading'; + const STATUS_PACKAGE_END = 'ready'; + const STATUS_ERROR = 'error'; + const HLS_BASE_URL = ''; protected string $basePath = '/tmp/'; @@ -45,32 +54,36 @@ class TranscodingV1 extends Worker public function init(): void { - $this->basePath .= $this->args['fileId'] . '/' . $this->args['profileId']; + $this->basePath .= $this->args['videoId'] . '/' . $this->args['profileId']; $this->inDir = $this->basePath . '/in/'; $this->outDir = $this->basePath . '/out/'; @mkdir($this->inDir, 0755, true); @mkdir($this->outDir, 0755, true); - $this->outPath = $this->outDir . $this->args['fileId']; /** TODO figure a way to write dir tree without this **/ + $this->outPath = $this->outDir . $this->args['videoId']; /** TODO figure a way to write dir tree without this **/ } public function run(): void { $project = new Document($this->args['project']); $this->database = $this->getProjectDB($project->getId()); - $profile = Authorization::skip(fn() => $this->database->findOne('video_profiles', [new Query('_uid', Query::TYPE_EQUAL, [$this->args['profileId']])])); - if($profile->isEmpty()){ - throw new Exception('No profile found'); + $sourceVideo = Authorization::skip(fn() => $this->database->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$this->args['videoId']])])); + if(empty($sourceVideo)){ + throw new Exception('Video not found'); + } + + $profile = Authorization::skip(fn() => $this->database->findOne('video_profiles', [new Query('_uid', Query::TYPE_EQUAL, [$this->args['profileId']])])); + if(empty($profile)){ + throw new Exception('profile not found'); } $user = new Document($this->args['user'] ?? []); - $bucket = Authorization::skip(fn() => $this->database->getDocument('buckets', $this->args['bucketId'])); - + $bucket = Authorization::skip(fn() => $this->database->getDocument('buckets', $sourceVideo['bucketId'])); if ($bucket->getAttribute('permission') === 'bucket') { - $file = Authorization::skip(fn() => $this->database->getDocument('bucket_' . $bucket->getInternalId(), $this->args['fileId'])); + $file = Authorization::skip(fn() => $this->database->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId'])); } else { - $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $this->args['fileId']); + $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $sourceVideo['fileId']); } $data = $this->getFilesDevice($project->getId())->read($file->getAttribute('path')); @@ -103,35 +116,47 @@ class TranscodingV1 extends Worker throw new Exception('Not an valid FFMpeg file "' . $inPath . '"'); } - //TODO Can you retranscode? + //Delete prev rendition $queries = [ - new Query('bucketId', Query::TYPE_EQUAL, [$this->args['bucketId']]), - new Query('fileId', Query::TYPE_EQUAL, [$this->args['fileId']]) + new Query('videoId', Query::TYPE_EQUAL, [$sourceVideo->getId()]), + new Query('profileId', Query::TYPE_EQUAL, [$profile->getId()]) ]; - $renditions = Authorization::skip(fn() => $this->database->find($collection, $queries, 12, 0, [], ['ASC'])); - if (!empty($renditions)) { - foreach ($renditions as $rendition) { - Authorization::skip(fn() => $this->database->deleteDocument($collection, $rendition->getId())); - } + $rendition = Authorization::skip(fn() => $this->database->findOne($collection, $queries)); + if (!empty($rendition)) { + Authorization::skip(fn() => $this->database->deleteDocument($collection, $rendition->getId())); $deviceFiles = $this->getVideoDevice($project->getId()); - $devicePath = $deviceFiles->getPath($this->args['fileId']); - $devicePath = str_ireplace($deviceFiles->getRoot(), $deviceFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $devicePath); - $deviceFiles->deletePath($devicePath); + if(!empty($rendition['path'])) { + $deviceFiles->deletePath($rendition['path']); + } } - $sourceInfo = $this->getVideoInfo($ffprobe->streams($inPath)); + $stream = !empty($rendition['stream']) ? $rendition['stream'] : 'hls'; + $general = $this->getMetadataExport($ffprobe->streams($inPath)); + var_dump($general); + if(!empty($general)) { + foreach ($general as $key => $value) { + $sourceVideo->setAttribute($key, $value); + } + + Authorization::skip(fn() => $this->database->updateDocument( + 'videos', + $sourceVideo->getId(), + $sourceVideo + )); + } + + $video = $ffmpeg->open($inPath); - foreach (['hls', 'dash'] as $stream) { - $query = Authorization::skip(function () use ($collection, $profile, $stream) { + + $query = Authorization::skip(function () use ($collection, $profile, $stream) { return $this->database->createDocument($collection, new Document([ - 'bucketId' => $this->args['bucketId'], - 'fileId' => $this->args['fileId'], + 'videoId' => $this->args['videoId'], 'profileId' => $profile->getId(), 'name' => $profile->getAttribute('name'), 'startedAt' => time(), - 'status' => 'started', + 'status' => self::STATUS_TRANSCODE_START, 'stream' => $stream, ])); }); @@ -156,11 +181,18 @@ class TranscodingV1 extends Worker list($general, $metadata) = $this->transcode($stream, $video, $format, $representation); + if (!empty($metadata)) { $query->setAttribute('metadata', json_encode($metadata)); } - $query->setAttribute('status', 'ended'); + if(!empty($general)) { + foreach ($general as $key => $value) { + $query->setAttribute($key, $value); + } + } + + $query->setAttribute('status', self::STATUS_TRANSCODE_END); $query->setAttribute('endedAt', time()); Authorization::skip(fn() => $this->database->updateDocument( $collection, @@ -182,13 +214,16 @@ class TranscodingV1 extends Worker } $deviceFiles = $this->getVideoDevice($project->getId()); - $devicePath = $deviceFiles->getPath($this->args['fileId']); + $devicePath = $deviceFiles->getPath($this->args['videoId']); $devicePath = str_ireplace($deviceFiles->getRoot(), $deviceFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $devicePath); $data = $this->getFilesDevice($project->getId())->read($this->outDir . $fileName); - $this->getVideoDevice($project->getId())->write($devicePath . DIRECTORY_SEPARATOR . $profile->getAttribute('name') . DIRECTORY_SEPARATOR . $fileName, $data, \mime_content_type($this->outDir . $fileName)); - var_dump($devicePath . DIRECTORY_SEPARATOR . $profile->getAttribute('name') . DIRECTORY_SEPARATOR . $fileName); + $renditionDir = $profile->getAttribute('width') . 'X' . $profile->getAttribute('height') . '@' .$profile->getAttribute('videoBitrate'); + $renditionPath = $devicePath . DIRECTORY_SEPARATOR . $renditionDir; + $this->getVideoDevice($project->getId())->write($renditionPath . DIRECTORY_SEPARATOR . $fileName, $data, \mime_content_type($this->outDir . $fileName)); + if ($start === 0) { - $query->setAttribute('status', 'uploading'); + $query->setAttribute('status', self::STATUS_UPLOADING); + $query->setAttribute('path', $renditionPath); Authorization::skip(fn() => $this->database->updateDocument( $collection, $query->getId(), @@ -208,7 +243,7 @@ class TranscodingV1 extends Worker @unlink($this->outDir . $fileName); } - $query->setAttribute('status', 'ready'); + $query->setAttribute('status', self::STATUS_PACKAGE_END); Authorization::skip(fn() => $this->database->updateDocument( $collection, $query->getId(), @@ -220,14 +255,13 @@ class TranscodingV1 extends Worker 'message' => $th->getMessage(), ])); - $query->setAttribute('status', 'error'); + $query->setAttribute('status', self::STATUS_ERROR); Authorization::skip(fn() => $this->database->updateDocument( $collection, $query->getId(), $query )); } - } } /** @@ -240,8 +274,7 @@ class TranscodingV1 extends Worker if (!empty($metadata['stream']['resolutions'][0])) { $general = $metadata['stream']['resolutions'][0]; - var_dump($general); - $parts = explode("x", $general); + $parts = explode("X", $general['dimension']); $info['width'] = $parts['0']; $info['height'] = $parts['1']; } @@ -251,12 +284,12 @@ class TranscodingV1 extends Worker if ($streams['codec_type'] === 'video') { $info['duration'] = $streams['duration']; $info['videoCodec'] = $streams['codec_name'] . ',' . $streams['codec_tag_string']; - $info['videoBitRate'] = $streams['bit_rate']; - $info['videoFrameRate'] = $streams['avg_frame_rate']; + $info['videoBitrate'] = $streams['bit_rate']; + $info['videoFramerate'] = $streams['avg_frame_rate']; } elseif ($streams['codec_type'] === 'audio') { $info['audioCodec'] = $streams['codec_name'] . ',' . $streams['codec_tag_string']; - $info['audioBitRate'] = $streams['sample_rate']; - $info['audioSamplRate'] = $streams['bit_rate']; + $info['audioBitrate'] = $streams['sample_rate']; + $info['audioSamplerate'] = $streams['bit_rate']; } } } @@ -295,7 +328,7 @@ class TranscodingV1 extends Worker return [ $this->getMetadataExport($dash->metadata()->export()), - ['dash' => !empty($xml) ? json_decode(json_encode((array)$xml), true) : []], + ['mpeg-dash' => !empty($xml) ? json_decode(json_encode((array)$xml), true) : []], ]; } @@ -307,7 +340,7 @@ class TranscodingV1 extends Worker ->setAdditionalParams($additionalParams) ->setHlsBaseUrl(self::HLS_BASE_URL) ->save($this->outPath); - + var_dump($this->outPath); return [ $this->getMetadataExport($hls->metadata()->export()), [] ]; diff --git a/src/Appwrite/Event/Transcoding.php b/src/Appwrite/Event/Transcoding.php index 01e393aa7d..2dac45e178 100644 --- a/src/Appwrite/Event/Transcoding.php +++ b/src/Appwrite/Event/Transcoding.php @@ -74,8 +74,7 @@ class Transcoding extends Event return Resque::enqueue($this->queue, $this->class, [ 'project' => $this->project, 'user' => $this->user, - 'bucketId' => $this->bucketId, - 'fileId' => $this->fileId, + 'videoId' => $this->videoId, 'profileId' => $this->profileId, ]); } diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 85e8a13975..5dae301e31 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -164,6 +164,7 @@ class Exception extends \Exception /** Video */ public const PROFILES_NOT_FOUND = 'profiles_not_found'; + public const VIDEO_NOT_FOUND = 'video_not_found'; private $type = ''; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index fc43c15d27..09cdd704a7 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -70,6 +70,8 @@ use Appwrite\Utopia\Response\Model\UsageProject; use Appwrite\Utopia\Response\Model\UsageStorage; use Appwrite\Utopia\Response\Model\UsageUsers; use Appwrite\Utopia\Response\Model\FileRendition; +use Appwrite\Utopia\Response\Model\Video; +use Appwrite\Utopia\Response\Model\VideoProfile; /** * @method Response setStatusCode(int $code = 200) @@ -128,6 +130,11 @@ class Response extends SwooleResponse public const MODEL_FILE_LIST = 'fileList'; public const MODEL_BUCKET = 'bucket'; public const MODEL_BUCKET_LIST = 'bucketList'; + + //video + public const MODEL_VIDEO = 'video'; + public const MODEL_VIDEO_PROFILE = 'videoProfile'; + public const MODEL_VIDEO_PROFILE_LIST = 'videoProfileList'; public const MODEL_FILE_RENDITION = 'fileRendition'; public const MODEL_FILE_RENDITIONS_LIST = 'fileRenditionsList'; @@ -221,7 +228,6 @@ class Response extends SwooleResponse ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) ->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG)) ->setModel(new BaseList('Files List', self::MODEL_FILE_LIST, 'files', self::MODEL_FILE)) - ->setModel(new BaseList('File Renditions List', self::MODEL_FILE_RENDITIONS_LIST, 'renditions', self::MODEL_FILE_RENDITION)) ->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET)) ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) @@ -241,6 +247,9 @@ class Response extends SwooleResponse ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) + ->setModel(new BaseList('File Renditions List', self::MODEL_FILE_RENDITIONS_LIST, 'renditions', self::MODEL_FILE_RENDITION)) + ->setModel(new BaseList('video profile List', self::MODEL_VIDEO_PROFILE_LIST, 'profiles', self::MODEL_VIDEO_PROFILE)) + // Entities ->setModel(new Collection()) ->setModel(new Attribute()) @@ -295,6 +304,8 @@ class Response extends SwooleResponse ->setModel(new UsageFunctions()) ->setModel(new UsageProject()) ->setModel(new FileRendition()) + ->setModel(new Video()) + ->setModel(new VideoProfile()) // Verification // Recovery diff --git a/src/Appwrite/Utopia/Response/Model/Video.php b/src/Appwrite/Utopia/Response/Model/Video.php new file mode 100644 index 0000000000..53c54ec003 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Video.php @@ -0,0 +1,59 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('bucketId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Bucket ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('fileId', [ + 'type' => self::TYPE_STRING, + 'description' => 'File ID.', + 'default' => '', + 'example' => 'd5fg5ehg1c168g7c', + ]) + ->addRule('size', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'File SIZE.', + 'default' => '', + 'example' => 3567790, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Video entity'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_VIDEO; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/videoProfile.php b/src/Appwrite/Utopia/Response/Model/videoProfile.php new file mode 100644 index 0000000000..f97f9d7436 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/videoProfile.php @@ -0,0 +1,78 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Video profile name.', + 'default' => '', + 'example' => '360P', + ]) + ->addRule('videoBitrate', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Video bitrate.', + 'default' => '', + 'example' => 3, + ]) + ->addRule('audioBitrate', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Audio bitrate.', + 'default' => '', + 'example' => 3, + ]) + ->addRule('width', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Video width.', + 'default' => '', + 'example' => 300, + ]) + ->addRule('height', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Video height.', + 'default' => '', + 'example' => 400, + ]) + + ->addRule('stream', [ + 'type' => self::TYPE_STRING, + 'description' => 'http video stream type.', + 'default' => '', + 'example' => 'mpeg-dash', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Video profile'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_VIDEO_PROFILE; + } +} diff --git a/tests/e2e/Services/Video/VideoCustomServerTest.php b/tests/e2e/Services/Video/VideoCustomServerTest.php index 30d917aa24..a1b7035cb0 100644 --- a/tests/e2e/Services/Video/VideoCustomServerTest.php +++ b/tests/e2e/Services/Video/VideoCustomServerTest.php @@ -13,9 +13,8 @@ class VideoCustomServerTest extends Scope use ProjectCustom; use SideServer; - public function testTranscoding(): array + public function testCreateBucketFile(): array { - $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -61,40 +60,80 @@ class VideoCustomServerTest extends Scope } @fclose($handle); - $pid = $this->getProject()['$id']; - $key = $this->getProject()['apiKey']; - $fid = $id; - $bid = $bucket['body']['$id']; + return [ + 'bucketId' => $bucket['body']['$id'], + 'fileId' => $id, + ]; + } - var_dump($pid); - var_dump($key); - var_dump($fid); - var_dump($bid); + /** + * @depends testCreateBucketFile + */ + public function testTranscodingRendition($data): array + { -// -// $pid = '62b1956c92e0e39ef577'; -// $key = 'f9b8dd3800b93a3dd513cf7cbcbd436cd61850b2c101662210e8ee2f052f796b3d4c7a08149634ce90da6037f6164d7faa36b32b91b568524e6720014e149b83f7a970c28de1a14a97a69010be325d142d51ca51f0f1b29783a7c1f4689d1b90a42cf19a7b55ec9ea6dc51974a1740b67e71de9f80009c2d91c6f3c686aa616c'; -// $fid = '62b1956e4f03f57a0f74'; -// $bid = '62b1956d0c600d70c8f7'; -// - $transcoding = $this->client->call(Client::METHOD_POST, '/video/buckets/' . $bid . '/files/' . $fid, [ + $response = $this->client->call(Client::METHOD_POST, '/video/buckets/' . $data['bucketId'] . '/files/' . $data['fileId'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $pid, - 'x-appwrite-key' => $key, + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'read' => ['role:all'], + 'write' => ['role:all'] + ]); + + $videoId = $response['body']['$id']; + + $response = $this->client->call(Client::METHOD_GET, '/video/profiles', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + + $profileId = $response['body']['profiles'][0]['$id']; + + $response = $this->client->call(Client::METHOD_POST, '/video/' . $videoId . '/rendition/' . $profileId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ 'read' => ['role:all'], 'write' => ['role:all'] ]); + var_dump($response['body']); + +// var_dump($pid); +// var_dump($key); +// var_dump($fid); +// var_dump($bid); +// +//// +//// $pid = '62b1956c92e0e39ef577'; +//// $key = 'f9b8dd3800b93a3dd513cf7cbcbd436cd61850b2c101662210e8ee2f052f796b3d4c7a08149634ce90da6037f6164d7faa36b32b91b568524e6720014e149b83f7a970c28de1a14a97a69010be325d142d51ca51f0f1b29783a7c1f4689d1b90a42cf19a7b55ec9ea6dc51974a1740b67e71de9f80009c2d91c6f3c686aa616c'; +//// $fid = '62b1956e4f03f57a0f74'; +//// $bid = '62b1956d0c600d70c8f7'; +//// +// $video = $this->client->call(Client::METHOD_POST, '/video/buckets/' . $bid . '/files/' . $fid, [ +// 'content-type' => 'application/json', +// 'x-appwrite-project' => $pid, +// 'x-appwrite-key' => $key, +// ], [ +// 'read' => ['role:all'], +// 'write' => ['role:all'] +// ]); +// var_dump($video); + + return [ - 'projectId' => $pid, - 'apiKey' => $key, - 'bucketId' => $bid, - 'fileId' => $fid, + 'videoId' => $videoId, + 'profileId' => $profileId, ]; } - + /** + * @depends testCreateBucketFile + */ public function testRenditions(): void {