From baea836574612ef46fbb18e8a10413478bf8066d Mon Sep 17 00:00:00 2001 From: shimon Date: Wed, 7 Sep 2022 11:16:38 +0300 Subject: [PATCH] sync with 0.16 --- app/config/collections.php | 2 +- app/config/errors.php | 35 +++ app/controllers/api/videos.php | 268 ++++++------------ app/views/videos/hls.phtml | 2 +- app/workers/transcoding.php | 29 +- src/Appwrite/Extend/Exception.php | 2 +- .../Services/Videos/VideoCustomServerTest.php | 16 +- 7 files changed, 145 insertions(+), 209 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 2c1098958b..b0608d61da 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -4060,7 +4060,7 @@ $collections = [ ], 'indexes' => [ [ - '$id' => '_key_unique', + '$id' => '_key_profile', 'type' => Database::INDEX_KEY, 'attributes' => ['protocol', 'width', 'height', 'videoBitrate', 'audioBitrate' ], 'lengths' => [], diff --git a/app/config/errors.php b/app/config/errors.php index e4dc3d5b4d..c5201a4ede 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -549,4 +549,39 @@ return [ 'description' => 'Domain verification for the requested domain has failed.', 'code' => 401, ], + Exception::VIDEO_PROFILE_NOT_FOUND => [ + 'name' => Exception::VIDEO_PROFILE_NOT_FOUND, + 'description' => 'Video profile not found.', + 'code' => 404, + ], + Exception::VIDEO_NOT_VALID => [ + 'name' => Exception::VIDEO_NOT_VALID, + 'description' => 'Video not valid.', + 'code' => 400, + ], + Exception::VIDEO_RENDITION_NOT_FOUND => [ + 'name' => Exception::VIDEO_RENDITION_NOT_FOUND, + 'description' => 'Video rendition not found.', + 'code' => 404, + ], + Exception::VIDEO_SUBTITLE_NOT_FOUND => [ + 'name' => Exception::VIDEO_SUBTITLE_NOT_FOUND, + 'description' => 'Video subtitle not found.', + 'code' => 404, + ], + Exception::VIDEO_NOT_FOUND => [ + 'name' => Exception::VIDEO_NOT_FOUND, + 'description' => 'Video not found.', + 'code' => 404, + ], + Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND => [ + 'name' => Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND, + 'description' => 'Video rendition segment not found', + 'code' => 404, + ], + Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND => [ + 'name' => Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND, + 'description' => 'Video subtitle segment not found', + 'code' => 404, + ], ]; diff --git a/app/controllers/api/videos.php b/app/controllers/api/videos.php index 68d0f8a387..3a9d7c12fb 100644 --- a/app/controllers/api/videos.php +++ b/app/controllers/api/videos.php @@ -4,6 +4,7 @@ use Appwrite\Auth\Auth; use Appwrite\Event\Delete; use Appwrite\Event\Transcoding; use Appwrite\Utopia\Database\Validator\CustomId; +use Appwrite\Utopia\Database\Validator\Queries\Files; use Appwrite\Utopia\Response; use Appwrite\Utopia\View; use Utopia\App; @@ -78,6 +79,7 @@ App::post('/v1/videos') ->action(action: function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, Document $user, string $mode) { $file = validateFilePermissions($dbForProject, $bucketId, $fileId, $mode, $user); + $video = Authorization::skip(function () use ($dbForProject, $bucketId, $file) { return $dbForProject->createDocument('videos', new Document([ 'bucketId' => $bucketId, @@ -85,6 +87,7 @@ App::post('/v1/videos') 'size' => $file->getAttribute('sizeOriginal'), ])); }); + $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($video, Response::MODEL_VIDEO); }); @@ -108,10 +111,9 @@ App::delete('/v1/videos/:videoId') ->inject('deletes') ->action(function (string $videoId, Response $response, Document $project, Database $dbForProject, string $mode, Document $user, Delete $deletes) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if ($video->isEmpty()) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } $deleted = $dbForProject->deleteDocument('videos', $videoId); @@ -147,14 +149,12 @@ App::put('/v1/videos/:videoId') ->inject('user') ->action(function (string $videoId, $bucketId, $fileId, Response $response, Document $project, Database $dbForProject, string $mode, Document $user) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if ($video->isEmpty()) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } $file = validateFilePermissions($dbForProject, $bucketId, $fileId, $mode, $user); - $video = Authorization::skip(function () use ($dbForProject, $videoId, $bucketId, $file) { return $dbForProject->updateDocument('videos', $videoId, new Document([ 'bucketId' => $bucketId, @@ -192,9 +192,8 @@ App::get('/v1/videos/:videoId') ->action(function (string $videoId, Response $response, Database $dbForProject) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); - if ($video->isEmpty()) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } $response->dynamic($video, Response::MODEL_VIDEO); @@ -211,36 +210,36 @@ App::get('/v1/videos') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_VIDEO) - ->param('limit', 25, new Range(0, 100), 'Maximum number of files to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) - ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true) - ->param('cursor', '', new UID(), 'ID of the file used as the starting point for the query, excluding the file itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true) - ->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true) - ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true) + ->param('queries', [], new Files(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Files::ALLOWED_ATTRIBUTES), true) ->inject('response') ->inject('dbForProject') - ->action(function (int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) { + ->action(function (array $queries, Response $response, Database $dbForProject) { - if (!empty($cursor)) { - $cursorFile = $dbForProject->getDocument('videos', $cursor); - - if ($cursorFile->isEmpty()) { - throw new Exception("File '{$cursor}' for the 'cursor' value not found.", 400, Exception::GENERAL_CURSOR_NOT_FOUND); + $queries = Query::parseQueries($queries); + $cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE); + $cursor = reset($cursor); + if ($cursor) { + /** @var Query $cursor */ + $fileId = $cursor->getValue(); + $cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('videos', $fileId)); + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File '{$fileId}' for the 'cursor' value not found."); } + + $cursor->setValue($cursorDocument); } - $videos = $dbForProject->find('videos', [], $limit, $offset, [], [$orderType], $cursorFile ?? null, $cursorDirection); - + $videos = $dbForProject->find('videos', $queries); if (empty($videos)) { throw new Exception('Videos not found', 404, Exception::VIDEO_NOT_FOUND); } $response->dynamic(new Document([ - 'total' => $dbForProject->count('videos', [], APP_LIMIT_COUNT), + 'total' => $dbForProject->count('videos', $queries, APP_LIMIT_COUNT), 'videos' => $videos, ]), Response::MODEL_VIDEO_LIST); }); - App::post('/v1/videos/:videoId/subtitles') ->desc('Add subtitle to video') ->groups(['api', 'video']) @@ -265,10 +264,9 @@ App::post('/v1/videos/:videoId/subtitles') ->inject('mode') ->action(action: function (string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Request $request, Response $response, Database $dbForProject, Document $user, string $mode) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if (empty($video)) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); @@ -289,7 +287,6 @@ App::post('/v1/videos/:videoId/subtitles') $response->dynamic($subtitle, Response::MODEL_SUBTITLE); }); - App::patch('/v1/videos/:videoId/subtitles/:subtitleId') ->desc('Update video subtitle') ->groups(['api', 'video']) @@ -313,9 +310,8 @@ App::patch('/v1/videos/:videoId/subtitles/:subtitleId') ->action(action: function (string $subtitleId, string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Response $response, Database $dbForProject) { $subtitle = Authorization::skip(fn() => $dbForProject->getDocument('videos_subtitles', $subtitleId)); - if ($subtitle->isEmpty()) { - throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND); + throw new Exception(Exception::VIDEO_SUBTITLE_NOT_FOUND); } $subtitle->setAttribute('videoId', $videoId) @@ -330,7 +326,6 @@ App::patch('/v1/videos/:videoId/subtitles/:subtitleId') $response->dynamic($subtitle, Response::MODEL_SUBTITLE); }); - App::delete('/v1/videos/:videoId/subtitles/:subtitleId') ->desc('Delete video subtitle') ->groups(['api', 'video']) @@ -349,16 +344,14 @@ App::delete('/v1/videos/:videoId/subtitles/:subtitleId') ->inject('mode') ->action(function (string $videoId, string $subtitleId, Response $response, Database $dbForProject, Document $user, string $mode) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); - - if ($video->isEmpty()) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); + if (empty($video)) { + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); $subtitle = Authorization::skip(fn() => $dbForProject->getDocument('videos_subtitles', $subtitleId)); - if ($subtitle->isEmpty()) { throw new Exception('Video subtitle not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND); } @@ -372,7 +365,6 @@ App::delete('/v1/videos/:videoId/subtitles/:subtitleId') $response->noContent(); }); - App::get('/v1/videos/:videoId/subtitles') ->desc('Get all video subtitles') ->groups(['api', 'video']) @@ -389,19 +381,21 @@ App::get('/v1/videos/:videoId/subtitles') ->inject('dbForProject') ->action(function ($videoId, Response $response, Database $dbForProject) { - $subtitles = Authorization::skip(fn () => $dbForProject->find('videos_subtitles', [new Query('videoId', Query::TYPE_EQUAL, [$videoId])])); + $query = [ + Query::equal('videoId', [$videoId]), + ]; + $subtitles = Authorization::skip(fn () => $dbForProject->find('videos_subtitles', $query)); if (empty($subtitles)) { - throw new Exception('Video subtitles not found', 404, Exception::VIDEO_SUBTITLE_NOT_FOUND); + throw new Exception(Exception::VIDEO_SUBTITLE_NOT_FOUND); } $response->dynamic(new Document([ - 'total' => $dbForProject->count('videos_subtitles', [], APP_LIMIT_COUNT), + 'total' => $dbForProject->count('videos_subtitles', $query, APP_LIMIT_COUNT), 'subtitles' => $subtitles, ]), Response::MODEL_SUBTITLE_LIST); }); - App::post('/v1/videos/:videoId/rendition') ->alias('/v1/videos/:videoId/rendition', []) ->desc('Create video rendition') @@ -423,23 +417,16 @@ App::post('/v1/videos/:videoId/rendition') ->inject('mode') ->action(action: function (string $videoId, string $profileId, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, string $mode) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [ - Query::equal('_uid', [$videoId]), - ])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if (empty($video)) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video->getAttribute('bucketId'), $video->getAttribute('fileId'), $mode, $user); - $profile = Authorization::skip(fn() => $dbForProject->findOne('videos_profiles', [ - Query::equal('_uid', [$profileId]), - ])); - - + $profile = Authorization::skip(fn() => $dbForProject->getDocument('videos_profiles', $profileId)); if (empty($profile)) { - throw new Exception('Video profile not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND); + throw new Exception(Exception::VIDEO_PROFILE_NOT_FOUND); } $transcoder = new Transcoding(); @@ -453,7 +440,6 @@ App::post('/v1/videos/:videoId/rendition') $response->noContent(); }); - App::delete('/v1/videos/:videoId/renditions/:renditionId') ->desc('Delete video rendition') ->groups(['api', 'video']) @@ -473,24 +459,22 @@ App::delete('/v1/videos/:videoId/renditions/:renditionId') ->inject('deviceVideos') ->action(function (string $videoId, string $renditionId, Response $response, Database $dbForProject, string $mode, Document $user, Device $deviceVideos) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); - - if ($video->isEmpty()) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); + if (empty($video)) { + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); $rendition = Authorization::skip(fn() => $dbForProject->getDocument('videos_renditions', $renditionId)); - if ($rendition->isEmpty()) { - throw new Exception('Video rendition not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND); + throw new Exception(Exception::VIDEO_RENDITION_NOT_FOUND); } $deleted = $dbForProject->deleteDocument('videos_renditions', $renditionId); if (!$deleted) { - throw new Exception('Failed to remove video rendition from DB', 500, Exception::GENERAL_SERVER_ERROR); + throw new Exception(Exception::GENERAL_SERVER_ERROR); } Authorization::skip(fn() => $dbForProject->deleteDocument('videos_renditions', $rendition->getId())); @@ -501,7 +485,6 @@ App::delete('/v1/videos/:videoId/renditions/:renditionId') $response->noContent(); }); - App::get('/v1/videos/:videoId/rendition/:renditionId') ->desc('Get all backet\'s videos') ->groups(['api', 'video']) @@ -521,17 +504,14 @@ App::get('/v1/videos/:videoId/rendition/:renditionId') ->inject('user') ->action(function ($videoId, $renditionId, Response $response, Database $dbForProject, string $mode, Document $user) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if ($video->isEmpty()) { throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); - - $rendition = Authorization::skip(fn () => $dbForProject->findOne('videos_renditions', [new Query('_uid', Query::TYPE_EQUAL, [$renditionId])])); - + $rendition = Authorization::skip(fn() => $dbForProject->getDocument('videos_renditions', $renditionId)); if ($rendition->isEmpty()) { throw new Exception('Video rendition not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND); } @@ -539,7 +519,6 @@ App::get('/v1/videos/:videoId/rendition/:renditionId') $response->dynamic($rendition, Response::MODEL_RENDITION); }); - App::get('/v1/videos/:videoId/renditions') ->desc('Get video renditions') ->groups(['api', 'video']) @@ -558,18 +537,16 @@ App::get('/v1/videos/:videoId/renditions') ->inject('user') ->action(function (string $videoId, Response $response, Database $dbForProject, string $mode, 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', 404, Exception::VIDEO_NOT_FOUND); + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); + if (empty($video)) { + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); $queries = [ - new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]), - new Query('endedAt', Query::TYPE_GREATER, [0]), - new Query('status', Query::TYPE_EQUAL, ['ready']), + Query::equal('videoId', [$video->getId()]), + Query::equal('status', ['ready']), ]; $renditions = Authorization::skip(fn () => $dbForProject->find('videos_renditions', $queries)); @@ -580,7 +557,6 @@ App::get('/v1/videos/:videoId/renditions') ]), Response::MODEL_RENDITION_LIST); }); - App::get('/v1/videos/:videoId/protocols/:protocolId') ->desc('Get video master renditions manifest') ->groups(['api', 'video']) @@ -599,12 +575,9 @@ App::get('/v1/videos/:videoId/protocols/:protocolId') ->inject('user') ->action(function (string $videoId, string $protocolId, Response $response, Database $dbForProject, string $mode, Document $user) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [ - Query::equal('_uid', [$videoId]), - ])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if (empty($video)) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); @@ -616,7 +589,7 @@ App::get('/v1/videos/:videoId/protocols/:protocolId') ])); if (empty($renditions)) { - throw new Exception('Renditions not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND); + throw new Exception(Exception::VIDEO_RENDITION_NOT_FOUND); } $baseUrl = 'http://127.0.0.1/v1/videos/' . $videoId . '/protocols/' . $protocolId; @@ -625,7 +598,7 @@ App::get('/v1/videos/:videoId/protocols/:protocolId') ])); $_renditions = []; - $_subtitles = []; + $_subtitles = []; if ($protocolId === 'hls') { foreach ($subtitles ?? [] as $subtitle) { @@ -693,10 +666,10 @@ App::get('/v1/videos/:videoId/protocols/:protocolId') $representation['attributes'] = $attributes['@attributes'] ?? []; $attributes = (array)$adaptation->Representation->SegmentList->attributes(); $representation['segmentList']['attributes'] = $attributes['@attributes'] ?? []; - $segments = Authorization::skip(fn() => $dbForProject->find('videos_renditions_segments', [ - new Query('renditionId', Query::TYPE_EQUAL, [$rendition->getId()]), - new Query('streamId', Query::TYPE_EQUAL, [$streamId]), - ], 1000, 0, ['streamId'])); + $segments = Authorization::skip(fn () => $dbForProject->find('videos_renditions_segments', [ + Query::equal('renditionId', [$rendition->getId()]), + Query::equal('streamId', [$streamId]), + ])); if (count($segments) === 0) { continue; @@ -743,7 +716,6 @@ App::get('/v1/videos/:videoId/protocols/:protocolId') } }); - App::get('/v1/videos/:videoId/protocols/:protocolId/renditions/:renditionId/streams/:streamId') ->desc('Get video rendition manifest') ->groups(['api', 'video']) @@ -765,21 +737,16 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/renditions/:renditionId/stre ->inject('user') ->action(function (string $videoId, string $protocolId, string $renditionId, string $streamId, Response $response, Database $dbForProject, string $mode, Document $user) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [ - Query::equal('_uid', [$videoId]), - ])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if (empty($video)) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); $rendition = Authorization::skip(fn () => $dbForProject->findOne('videos_renditions', [ Query::equal('_uid', [$renditionId]), - Query::equal('videoId', [$video->getId()]), Query::equal('status', ['ready']), - Query::equal('protocol', [$protocolId]), ])); if (empty($rendition)) { @@ -805,7 +772,7 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/renditions/:renditionId/stre $template = new View(__DIR__ . '/../../views/videos/hls.phtml'); $template->setParam('targetDuration', $rendition->getAttribute('targetDuration')); - $template->setParam('paramsSegments', $_segments); + $template->setParam('segments', $_segments); $response->setContentType('application/x-mpegurl') ->send($template->render(false)); }); @@ -833,37 +800,9 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/renditions/:renditionId/segm ->inject('user') ->action(function (string $videoId, string $protocolId, string $renditionId, string $segmentId, Response $response, Database $dbForProject, Device $deviceVideos, string $mode, Document $user) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [ - Query::equal('_uid', [$videoId]), - ])); - - if (empty($video)) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); - } - - validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); - - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [ - Query::equal('_uid', [$videoId]), - ])); - - $rendition = Authorization::skip(fn () => $dbForProject->findOne('videos_renditions', [ - Query::equal('_uid', [$renditionId]), - Query::equal('videoId', [$video->getId()]), - Query::equal('status', ['ready']), - Query::equal('protocol', [$protocolId]), - ])); - - if (empty($rendition)) { - throw new Exception('Rendition not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND); - } - - $segment = Authorization::skip(fn () => $dbForProject->findOne('videos_renditions_segments', [ - Query::equal('_uid', [$segmentId]), - ])); - + $segment = Authorization::skip(fn() => $dbForProject->getDocument('videos_renditions_segments', $segmentId)); if (empty($segment)) { - throw new Exception('Rendition segments not found', 404, Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND); + throw new Exception('segment not found', 404, Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND); } $output = $deviceVideos->read($segment->getAttribute('path') . $segment->getAttribute('fileName')); @@ -899,19 +838,16 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/subtitles/:subtitleId') ->inject('user') ->action(function (string $videoId, string $protocolId, string $subtitleId, Response $response, Database $dbForProject, Device $deviceVideos, string $mode, Document $user) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [ - new Query('_uid', Query::TYPE_EQUAL, [$videoId]) - ])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if (empty($video)) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); - $subtitle = Authorization::skip(fn() => $dbForProject->findOne('videos_subtitles', [ - new Query('_uid', Query::TYPE_EQUAL, [$subtitleId]), - new Query('status', Query::TYPE_EQUAL, ['ready']) + $subtitle = Authorization::skip(fn () => $dbForProject->find('videos_subtitles', [ + Query::equal('_uid', [$subtitleId]), + Query::equal('status', ['ready']), ])); if (empty($subtitle)) { @@ -919,17 +855,14 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/subtitles/:subtitleId') } if ($protocolId == 'hls') { - $segments = Authorization::skip(fn() => $dbForProject->find('videos_subtitles_segments', [ - new Query('subtitleId', Query::TYPE_EQUAL, [$subtitleId]), - ], 4000)); - + $segments = Authorization::skip(fn() => $dbForProject->getDocument('videos_subtitles_segments', $subtitleId)); if (empty($segments)) { throw new Exception('Subtitle segments not found', 404, Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND); } - $paramsSegments = []; + $_segments = []; foreach ($segments as $segment) { - $paramsSegments[] = [ + $_segments[] = [ 'duration' => $segment->getAttribute('duration'), 'url' => 'http://127.0.0.1/v1/videos/' . $videoId . '/protocols/' . $protocolId . '/subtitles/' . $subtitleId . '/segments/' . $segment->getId(), ]; @@ -937,11 +870,13 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/subtitles/:subtitleId') $template = new View(__DIR__ . '/../../views/videos/hls-subtitles.phtml'); $template->setParam('targetDuration', $subtitle->getAttribute('targetDuration')); - $template->setParam('paramsSegments', $paramsSegments); - $response->setContentType('application/x-mpegurl')->send($template->render(false)); + $template->setParam('paramsSegments', $_segments); + $response->setContentType('application/x-mpegurl') + ->send($template->render(false)); } else { $output = $deviceVideos->read($deviceVideos->getPath($subtitle->getAttribute('videoId')) . '/' . $subtitle->getId() . '.vtt'); - $response->setContentType('text/vtt')->send($output); + $response->setContentType('text/vtt') + ->send($output); } }); @@ -968,37 +903,23 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/subtitles/:subtitleId/segmen ->inject('user') ->action(function (string $videoId, string $protocolId, string $subtitleId, string $segmentId, Response $response, Database $dbForProject, Device $deviceVideos, string $mode, Document $user) { - $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [ - new Query('_uid', Query::TYPE_EQUAL, [$videoId]) - ])); - + $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if (empty($video)) { - throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user); - $subtitle = Authorization::skip(fn() => $dbForProject->findOne('videos_subtitles', [ - new Query('_uid', Query::TYPE_EQUAL, [$subtitleId]), - new Query('status', Query::TYPE_EQUAL, ['ready']) - ])); - - if (empty($subtitle)) { - throw new Exception('subtitle not found', 404, Exception::VIDEO_SUBTITLE_NOT_FOUND); - } - - $segment = Authorization::skip(fn () => $dbForProject->findOne('videos_subtitles_segments', [ - new Query('_uid', Query::TYPE_EQUAL, [$segmentId])])); - + $segment = Authorization::skip(fn() => $dbForProject->getDocument('videos_subtitles_segments', $segmentId)); if (empty($segment)) { - throw new Exception('Subtitle segments not found', 404, Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND); + throw new Exception(Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND); } $output = $deviceVideos->read($segment->getAttribute('path') . $segment->getAttribute('fileName')); - $response->setContentType('text/vtt')->send($output); + $response->setContentType('text/vtt') + ->send($output); }); - App::post('/v1/videos/profiles') ->desc('Create video profile') ->groups(['api', 'video']) @@ -1019,7 +940,7 @@ App::post('/v1/videos/profiles') ->inject('response') ->inject('dbForProject') ->action(action: function (string $name, string $videoBitrate, string $audioBitrate, string $width, string $height, string $protocol, Response $response, Database $dbForProject) { - try { + $profile = Authorization::skip(function () use ($dbForProject, $name, $videoBitrate, $audioBitrate, $width, $height, $protocol) { return $dbForProject->createDocument('videos_profiles', new Document([ 'name' => $name, @@ -1030,15 +951,11 @@ App::post('/v1/videos/profiles') 'protocol' => $protocol, ])); }); - } catch (DuplicateException $exception) { - throw new Exception('Profile already exists', 409, Exception::VIDEO_PROFILE_ALREADY_EXISTS); - } $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($profile, Response::MODEL_PROFILE); }); - App::patch('/v1/videos/profiles/:profileId') ->desc('Update video profile') ->groups(['api', 'video']) @@ -1062,9 +979,8 @@ App::patch('/v1/videos/profiles/:profileId') ->action(action: function (string $profileId, string $name, string $videoBitrate, string $audioBitrate, string $width, string $height, string $protocol, Response $response, Database $dbForProject) { $profile = Authorization::skip(fn() => $dbForProject->getDocument('videos_profiles', $profileId)); - ; if ($profile->isEmpty()) { - throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND); + throw new Exception(Exception::PROJECT_NOT_FOUND); } $profile->setAttribute('name', $name) @@ -1079,7 +995,6 @@ App::patch('/v1/videos/profiles/:profileId') $response->dynamic($profile, Response::MODEL_PROFILE); }); - App::get('/v1/videos/profiles/:profileId') ->desc('Get video profile') ->groups(['api', 'video']) @@ -1097,15 +1012,13 @@ App::get('/v1/videos/profiles/:profileId') ->action(function (string $profileId, Response $response, Database $dbForProject) { $profile = Authorization::skip(fn() => $dbForProject->getDocument('videos_profiles', $profileId)); - if ($profile->isEmpty()) { - throw new Exception('Video profile not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND); + throw new Exception(Exception::VIDEO_PROFILE_NOT_FOUND); } $response->dynamic($profile, Response::MODEL_PROFILE); }); - App::get('/v1/videos/profiles') ->desc('Get all video profiles') ->groups(['api', 'video']) @@ -1121,10 +1034,9 @@ App::get('/v1/videos/profiles') ->inject('dbForProject') ->action(function (Response $response, Database $dbForProject) { - $profiles = Authorization::skip(fn () => $dbForProject->find('videos_profiles', [], 12, 0, [], ['ASC'])); - + $profiles = Authorization::skip(fn () => $dbForProject->find('videos_profiles')); if (empty($profiles)) { - throw new Exception('Video profiles where not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND); + throw new Exception(Exception::VIDEO_PROFILE_NOT_FOUND); } $response->dynamic(new Document([ @@ -1133,7 +1045,6 @@ App::get('/v1/videos/profiles') ]), Response::MODEL_PROFILE_LIST); }); - App::delete('/v1/videos/profiles/:profileId') ->desc('Delete video profile') ->groups(['api', 'video']) @@ -1150,9 +1061,8 @@ App::delete('/v1/videos/profiles/:profileId') ->action(function (string $profileId, Response $response, Database $dbForProject) { $profile = Authorization::skip(fn() => $dbForProject->getDocument('videos_profiles', $profileId)); - if ($profile->isEmpty()) { - throw new Exception('Video profile not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND); + throw new Exception(Exception::VIDEO_PROFILE_NOT_FOUND); } $deleted = $dbForProject->deleteDocument('videos_profiles', $profileId); diff --git a/app/views/videos/hls.phtml b/app/views/videos/hls.phtml index 21d9a720d8..f401254f98 100644 --- a/app/views/videos/hls.phtml +++ b/app/views/videos/hls.phtml @@ -3,7 +3,7 @@ #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:getParam('targetDuration', 0)?> #EXT-X-MEDIA-SEQUENCE:0 -getParam('paramsSegments', []) as $segment): ?> +getParam('segments', []) as $segment): ?> #EXTINF: diff --git a/app/workers/transcoding.php b/app/workers/transcoding.php index 99c72891e9..8ad919b24a 100644 --- a/app/workers/transcoding.php +++ b/app/workers/transcoding.php @@ -80,7 +80,7 @@ class TranscodingV1 extends Worker ])); if (empty($sourceVideo)) { - throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND); + throw new Exception(Exception::VIDEO_NOT_FOUND); } $profile = Authorization::skip(fn() => $this->database->findOne('videos_profiles', [ @@ -88,7 +88,7 @@ class TranscodingV1 extends Worker ])); if (empty($profile)) { - throw new Exception('Video profile not found', 400, Exception::VIDEO_PROFILE_NOT_FOUND); + throw new Exception(Exception::VIDEO_PROFILE_NOT_FOUND); } $bucket = Authorization::skip( @@ -104,7 +104,7 @@ class TranscodingV1 extends Worker $result = $this->writeData($project, $file); if (empty($result)) { - throw new Exception('File write failed', 500, Exception::GENERAL_UNKNOWN); + throw new Exception(Exception::GENERAL_UNKNOWN); } $ffprobe = FFMpeg\FFProbe::create(); @@ -114,7 +114,7 @@ class TranscodingV1 extends Worker ]); if (!$ffprobe->isValid($inPath)) { - throw new Exception('Not an valid FFMpeg file "' . $inPath . '"'); + throw new Exception('Not an valid Video file "' . $inPath . '"'); } foreach ($ffprobe->streams($inPath)->audios()->getIterator() as $stream) { @@ -185,16 +185,16 @@ class TranscodingV1 extends Worker ]; } - $query = Authorization::skip(function () use ($profile) { - return $this->database->createDocument('videos_renditions', new Document([ - 'videoId' => $this->args['videoId'], - 'profileId' => $profile->getId(), - 'name' => $this->getRenditionName(), - 'startedAt' => DateTime::now(), - 'status' => self::STATUS_START, - 'protocol' => $profile->getAttribute('protocol'), - ])); - }); + $query = Authorization::skip(function () use ($profile) { + return $this->database->createDocument('videos_renditions', new Document([ + 'videoId' => $this->args['videoId'], + 'profileId' => $profile->getId(), + 'name' => $this->getRenditionName(), + 'startedAt' => DateTime::now(), + 'status' => self::STATUS_START, + 'protocol' => $profile->getAttribute('protocol'), + ])); + }); $renditionRootPath = $this->getVideoDevice($project->getId())->getPath($this->args['videoId']) . '/'; $renditionPath = $renditionRootPath . $this->getRenditionName() . '-' . $query->getId() . '/'; @@ -315,6 +315,7 @@ class TranscodingV1 extends Worker $this->getVideoDevice($project->getId())->write($to . $fileName, $data, \mime_content_type($this->outDir . $fileName)); if ($start === 0) { + $query->setAttribute('progress', '100'); $query->setAttribute('status', self::STATUS_UPLOADING); $query->setAttribute('path', $renditionPath); Authorization::skip(fn() => $this->database->updateDocument('videos_renditions', $query->getId(), $query)); diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 1314b0d525..a542e4c330 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -179,7 +179,7 @@ class Exception extends \Exception /** Video */ public const VIDEO_PROFILE_NOT_FOUND = 'video_profile_not_found'; - public const VIDEO_PROFILE_ALREADY_EXISTS = 'video_profile_already_exists'; + public const VIDEO_NOT_VALID = 'video_not_valid'; public const VIDEO_RENDITION_NOT_FOUND = 'video_rendition_not_found'; public const VIDEO_SUBTITLE_NOT_FOUND = 'video_subtitle_not_found'; public const VIDEO_NOT_FOUND = 'video_not_found'; diff --git a/tests/e2e/Services/Videos/VideoCustomServerTest.php b/tests/e2e/Services/Videos/VideoCustomServerTest.php index f9436a63a7..d199a124a8 100644 --- a/tests/e2e/Services/Videos/VideoCustomServerTest.php +++ b/tests/e2e/Services/Videos/VideoCustomServerTest.php @@ -31,16 +31,6 @@ class VideoCustomServerTest extends Scope 'protocol' => 'hls', ]); - $x = [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - - ]; - var_dump($x); - exit; - $profileId = $response['body']['$id']; $this->assertEquals(201, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); @@ -115,7 +105,7 @@ class VideoCustomServerTest extends Scope $this->assertEquals(404, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertEquals('Video profile not found', $response['body']['message']); + $this->assertEquals('Video profile not found.', $response['body']['message']); $response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [ 'content-type' => 'application/json', @@ -159,7 +149,7 @@ class VideoCustomServerTest extends Scope $this->assertEquals(404, $response['headers']['status-code']); $this->assertNotEmpty($response['body']); - $this->assertEquals('Video profile not found', $response['body']['message']); + $this->assertEquals('Video profile not found.', $response['body']['message']); } /** @@ -360,7 +350,6 @@ class VideoCustomServerTest extends Scope $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); - /** * Try to transcode with wrong profileId */ @@ -849,6 +838,7 @@ class VideoCustomServerTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], + 'queries' => [ 'limit(1)' ], ]); $this->assertEquals(200, $response['headers']['status-code']);