mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
refactor worker flow
This commit is contained in:
+45
-34
@@ -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],
|
||||
],
|
||||
|
||||
@@ -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'
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
+132
-25
@@ -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']);
|
||||
});
|
||||
|
||||
+75
-42
@@ -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()), []
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Video extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class VideoProfile extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
|
||||
Reference in New Issue
Block a user