mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
view template
This commit is contained in:
+6
-1
@@ -50,7 +50,8 @@ RUN \
|
||||
yaml-dev \
|
||||
imagemagick \
|
||||
imagemagick-dev \
|
||||
libmaxminddb-dev
|
||||
libmaxminddb-dev \
|
||||
ffmpeg
|
||||
|
||||
RUN docker-php-ext-install sockets
|
||||
|
||||
@@ -234,6 +235,7 @@ RUN \
|
||||
docker-cli \
|
||||
docker-compose \
|
||||
libgomp \
|
||||
ffmpeg \
|
||||
&& docker-php-ext-install sockets opcache pdo_mysql \
|
||||
&& apk del .deps \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
@@ -264,12 +266,14 @@ COPY ./src /usr/src/code/src
|
||||
|
||||
# Set Volumes
|
||||
RUN mkdir -p /storage/uploads && \
|
||||
mkdir -p /storage/video && \
|
||||
mkdir -p /storage/cache && \
|
||||
mkdir -p /storage/config && \
|
||||
mkdir -p /storage/certificates && \
|
||||
mkdir -p /storage/functions && \
|
||||
mkdir -p /storage/debug && \
|
||||
chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \
|
||||
chown -Rf www-data.www-data /storage/video && chmod -Rf 0755 /storage/video && \
|
||||
chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \
|
||||
chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \
|
||||
chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \
|
||||
@@ -297,6 +301,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/worker-functions && \
|
||||
chmod +x /usr/local/bin/worker-builds && \
|
||||
chmod +x /usr/local/bin/worker-mails && \
|
||||
chmod +x /usr/local/bin/worker-transcoding && \
|
||||
chmod +x /usr/local/bin/worker-webhooks
|
||||
|
||||
# Letsencrypt Permissions
|
||||
|
||||
@@ -2845,6 +2845,132 @@ $collections = [
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos' => [
|
||||
'$collection' => 'video_renditions',
|
||||
'$id' => 'video_renditions',
|
||||
'$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',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'renditionId',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'renditionName',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 2048,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'startedAt',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'endedAt',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'metadata',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384, // https://tools.ietf.org/html/rfc4288#section-4.2
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
[
|
||||
'$id' => 'status',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 100,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'progress',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 4,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'stream',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_bucket_file',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['bucketId','fileId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
return $collections;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
'id' => 1,
|
||||
'name' => '360p',
|
||||
'videoBitrate' => 890, //video bitrate in Kbps
|
||||
'audioBitrate' => 64, //audio bitrate in Kbps
|
||||
'width' => 640, //width resolution in px
|
||||
'height' => 360, //height resolution in px
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'name' => '576p',
|
||||
'videoBitrate' => 2538,
|
||||
'audioBitrate' => 128,
|
||||
'width' => 1024,
|
||||
'height' => 576,
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'name' => '720p',
|
||||
'videoBitrate' => 3551,
|
||||
'audioBitrate' => 128,
|
||||
'width' => 1280,
|
||||
'height' => 720,
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\ClamAV\Network;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Transcoding;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\Stats\Stats;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\View;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Exception\Structure as StructureException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Storage\Compression\Algorithms\GZIP;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Storage\Validator\File;
|
||||
use Utopia\Storage\Validator\FileExt;
|
||||
use Utopia\Storage\Validator\FileSize;
|
||||
use Utopia\Storage\Validator\Upload;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\HexColor;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\Swoole\Request;
|
||||
use Streaming\Representation;
|
||||
|
||||
App::post('/v1/video/buckets/:bucketId/files/:fileId')
|
||||
->alias('/v1/video/files', ['bucketId' => 'default'])
|
||||
->desc('Start transcoding video')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.write')
|
||||
// ->label('event', 'buckets.[bucketId].files.[fileId].create')
|
||||
->param('bucketId', 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('fileId', '', 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 $bucketId, string $fileId, ?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 */
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$transcoder = new Transcoding();
|
||||
$transcoder
|
||||
->setUser($user)
|
||||
->setProject($project)
|
||||
->setBucketId($bucketId)
|
||||
->setFileId($fileId)
|
||||
->trigger();
|
||||
|
||||
$response->json(['result' => 'ok']);
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/video/buckets/:bucketId/files/:fileId/renditions')
|
||||
->alias('/v1/storage/files/:fileId/renditions', ['bucketId' => 'default'])
|
||||
->desc('Get File renditions')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'getFile')
|
||||
->label('sdk.description', '/docs/references/storage/get-file.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_FILE)
|
||||
->param('bucketId', 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('fileId', '', new UID(), 'File ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('usage')
|
||||
->inject('mode')
|
||||
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Stats $usage, string $mode) {
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $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
|
||||
if ($bucket->getAttribute('permission') === 'bucket') {
|
||||
$validator = new Authorization('read');
|
||||
if (!$validator->isValid($bucket->getRead())) {
|
||||
throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
if ($bucket->getAttribute('permission') === 'bucket') {
|
||||
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
|
||||
} else {
|
||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
|
||||
}
|
||||
|
||||
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
|
||||
throw new Exception('File not found', 404, Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = [
|
||||
new Query('bucketId', Query::TYPE_EQUAL, [$bucketId]),
|
||||
new Query('fileId', Query::TYPE_EQUAL, [$fileId]),
|
||||
new Query('stream', Query::TYPE_EQUAL, ['dash']),
|
||||
];
|
||||
|
||||
$renditions = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId() . '_video_renditions', $queries, 12, 0, [], ['ASC']));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $dbForProject->count('bucket_' . $bucket->getInternalId() . '_video_renditions', $queries, APP_LIMIT_COUNT),
|
||||
'renditions' => $renditions,
|
||||
]), Response::MODEL_FILE_RENDITIONS_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/video/buckets/:bucketId/files/:stream/:fileId')
|
||||
->alias('/v1/video/buckets/:bucketId/files/:stream/:fileId', [])
|
||||
->desc('Get video playlist manifest')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.read')
|
||||
->param('bucketId', 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('stream', '', new WhiteList(['hls', 'dash']), 'stream protocol name')
|
||||
->param('fileId', '', new UID(), 'File ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('usage')
|
||||
->inject('mode')
|
||||
->action(function (string $bucketId, string $stream, string $fileId, Response $response, Database $dbForProject, Stats $usage, string $mode) {
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $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
|
||||
if ($bucket->getAttribute('permission') === 'bucket') {
|
||||
$validator = new Authorization('read');
|
||||
if (!$validator->isValid($bucket->getRead())) {
|
||||
throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
if ($bucket->getAttribute('permission') === 'bucket') {
|
||||
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
|
||||
} else {
|
||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
|
||||
}
|
||||
|
||||
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
|
||||
throw new Exception('File not found', 404, Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = [
|
||||
new Query('bucketId', Query::TYPE_EQUAL, [$bucketId]),
|
||||
new Query('fileId', Query::TYPE_EQUAL, [$fileId]),
|
||||
new Query('stream', Query::TYPE_EQUAL, [$stream]),
|
||||
new Query('endedAt', Query::TYPE_GREATER, [0]),
|
||||
new Query('status', Query::TYPE_EQUAL, ['ready'])
|
||||
];
|
||||
|
||||
$renditions = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId() . '_video_renditions', $queries, 12, 0, [], ['ASC']));
|
||||
|
||||
if (empty($renditions)) {
|
||||
throw new Exception('Renditions not found');
|
||||
}
|
||||
|
||||
if ($stream === 'hls') {
|
||||
foreach ($renditions as $rendition) {
|
||||
$metadata = $rendition->getAttribute('metadata');
|
||||
$fileId = $rendition->getAttribute('fileId');
|
||||
$t['bandwidth'] = (($metadata['general']['video']['bitrate'] + $metadata['general']['audio']['bitrate']) * 1024);
|
||||
$t['resolution'] = $metadata['general']['resolution'];
|
||||
$t['name'] = $rendition->getAttribute('renditionName');
|
||||
$t['path'] = $rendition->getAttribute('renditionName') . DIRECTORY_SEPARATOR . $fileId . '_' . $rendition->getAttribute('renditionName') . '.m3u8';
|
||||
$params[] = $t;
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/video/hls.phtml');
|
||||
$template->setParam('params', $params);
|
||||
$response->setContentType('application/x-mpegurl');
|
||||
$response->send($template->render());
|
||||
} else {
|
||||
$adaptations = [];
|
||||
foreach ($renditions as $rendition) {
|
||||
$metadata = $rendition->getAttribute('metadata');
|
||||
foreach ($metadata['dash']['Period']['AdaptationSet'] as $set) {
|
||||
$adaption = $set['@attributes'];
|
||||
$adaption['baseUrl'] = $rendition->getAttribute('renditionName') . DIRECTORY_SEPARATOR;
|
||||
$adaption['representation'] = $set['Representation']['@attributes'];
|
||||
$adaption['representation']['SegmentTemplate'] = $set['Representation']['SegmentTemplate']['@attributes'];
|
||||
$adaption['representation']['segmentTemplate']['segmentTimeline'] = $set['Representation']['SegmentTemplate']['SegmentTimeline'];
|
||||
$adaptations[] = $adaption;
|
||||
}
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/video/dash.phtml');
|
||||
$template->setParam('params', $adaptations);
|
||||
$response->setContentType('application/dash+xml');
|
||||
$response->send($template->render());
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
$params = $this->getParam('params', []);
|
||||
?>
|
||||
<?php echo '<?xml version="1.0" encoding="utf-8"?>'; ?>
|
||||
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
|
||||
profiles="urn:mpeg:dash:profile:isoff-live:2011" type="static" mediaPresentationDuration="PT13.2S" maxSegmentDuration="PT6.0S" minBufferTime="PT16.6S">
|
||||
<ProgramInformation></ProgramInformation>
|
||||
<ServiceDescription id="0"></ServiceDescription>
|
||||
<Period id="0" start="PT0.0S">
|
||||
<?php foreach ($this->getParam('params', []) as $param): ?>
|
||||
<AdaptationSet
|
||||
<?php if (isset($param['id'])):?> id="<?php echo $param['id']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['contentType'])): ?> contentType="<?php echo $param['contentType']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['startWithSAP'])): ?> startWithSAP="<?php echo $param['startWithSAP']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['segmentAlignment'])): ?> segmentAlignment="<?php echo $param['segmentAlignment']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['bitstreamSwitching'])): ?> bitstreamSwitching="<?php echo $param['bitstreamSwitching']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['frameRate'])): ?> frameRate="<?php echo $param['frameRate']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['maxWidth'])): ?> maxWidth="<?php echo $param['maxWidth']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['par'])): ?> par="<?php echo $param['par']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['lang'])): ?> lang="<?php echo $param['lang']; ?>" <?php endif;?>
|
||||
>
|
||||
<BaseURL><?php if (isset($param['baseUrl'])): echo $param['baseUrl']; endif;?></BaseURL>
|
||||
<Representation
|
||||
<?php if (isset($param['representation']['id'])):?> id="<?php echo $param['representation']['id']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['mimeType'])):?> mimeType="<?php echo $param['representation']['mimeType']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['codecs'])):?> codecs="<?php echo $param['representation']['codecs']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['width'])):?> width="<?php echo $param['representation']['width']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['height'])):?> height="<?php echo $param['representation']['height']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['sar'])):?> sar="<?php echo $param['representation']['sar']; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['audioSamplingRate'])):?> audioSamplingRate="<?php echo $param['representation']['audioSamplingRate']; ?>" <?php endif;?>
|
||||
>
|
||||
<?php if ($param['contentType'] === 'audio'):?>
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
|
||||
<?php endif;?>
|
||||
<SegmentTemplate
|
||||
<?php if (isset($param['representation']['segmentTemplate']['timescale'] )):?> timescale="<?php echo ['representation']['segmentTemplate']['timescale'] ; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['segmentTemplate']['initialization'] )):?> initialization="<?php echo ['representation']['segmentTemplate']['initialization'] ; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['segmentTemplate']['media'] )):?> media="<?php echo ['representation']['segmentTemplate']['media'] ; ?>" <?php endif;?>
|
||||
<?php if (isset($param['representation']['segmentTemplate']['startNumber'] )):?> startNumber="<?php echo ['representation']['segmentTemplate']['startNumber'] ; ?>" <?php endif;?>
|
||||
>
|
||||
<SegmentTimeline>
|
||||
<?php foreach ($param['representation']['segmentTemplate']['segmentTimeline']['S'] as $s): ?>
|
||||
<S
|
||||
<?php if (isset($s['@attributes']['t'])):?> t="<?php echo $s['@attributes']['t']; ?> " <?php endif;?>
|
||||
<?php if (isset($s['@attributes']['d'])):?> d="<?php echo $s['@attributes']['d']; ?>" <?php endif;?>
|
||||
/>
|
||||
<?php endforeach; ?>
|
||||
</SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
<?php endforeach; ?>
|
||||
</Period>
|
||||
</MPD>
|
||||
@@ -0,0 +1,7 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
<?php foreach ($this->getParam('params', []) as $param): ?>
|
||||
#EXT-X-STREAM-INF:BANDWIDTH="<?php echo $param['bandwidth']; ?>" ,RESOLUTION="<?php echo $param['resolution']; ?>" ,NAME="<?php echo $param['name']; ?>"<?php echo PHP_EOL?>
|
||||
<?php echo $param['path'] ?>
|
||||
<?php endforeach; ?>
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Streaming\Format\StreamFormat;
|
||||
use Streaming\Media;
|
||||
use Streaming\Representation;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use FFMpeg\FFProbe\DataMapping\StreamCollection;
|
||||
use Utopia\Storage\Compression\Algorithms\GZIP;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
Console::title('Transcoding V1 Worker');
|
||||
Console::success(APP_NAME . ' transcoding worker v1 has started');
|
||||
|
||||
class TranscodingV1 extends Worker
|
||||
{
|
||||
const HLS_BASE_URL = '';
|
||||
|
||||
protected string $basePath = '/tmp/';
|
||||
|
||||
protected string $inDir;
|
||||
|
||||
protected string $outDir;
|
||||
|
||||
protected string $outPath;
|
||||
|
||||
protected Database $database;
|
||||
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return "Transcoding";
|
||||
}
|
||||
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
|
||||
$this->basePath .= $this->args['fileId'];
|
||||
$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 **/
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$project = new Document($this->args['project']);
|
||||
$user = new Document($this->args['user'] ?? []);
|
||||
$this->database = $this->getProjectDB($project->getId());
|
||||
|
||||
$bucket = Authorization::skip(fn() => $this->database->getDocument('buckets', $this->args['bucketId']));
|
||||
|
||||
if ($bucket->getAttribute('permission') === 'bucket') {
|
||||
$file = Authorization::skip(fn() => $this->database->getDocument('bucket_' . $bucket->getInternalId(), $this->args['fileId']));
|
||||
} else {
|
||||
$file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $this->args['fileId']);
|
||||
}
|
||||
|
||||
$data = $this->getFilesDevice($project->getId())->read($file->getAttribute('path'));
|
||||
$fileName = basename($file->getAttribute('path'));
|
||||
$inPath = $this->inDir . $fileName;
|
||||
$collection = 'bucket_' . $bucket->getInternalId() . '_video_renditions';
|
||||
|
||||
if (!empty($file->getAttribute('openSSLCipher'))) { // Decrypt
|
||||
$data = OpenSSL::decrypt(
|
||||
$data,
|
||||
$file->getAttribute('openSSLCipher'),
|
||||
App::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
|
||||
0,
|
||||
\hex2bin($file->getAttribute('openSSLIV')),
|
||||
\hex2bin($file->getAttribute('openSSLTag'))
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($file->getAttribute('algorithm', ''))) {
|
||||
$compressor = new GZIP();
|
||||
$data = $compressor->decompress($data);
|
||||
}
|
||||
|
||||
$this->getFilesDevice($project->getId())->write($this->inDir . $fileName, $data, $file->getAttribute('mimeType'));
|
||||
|
||||
$ffprobe = FFMpeg\FFProbe::create([]);
|
||||
$ffmpeg = Streaming\FFMpeg::create([]);
|
||||
|
||||
if (!$ffprobe->isValid($inPath)) {
|
||||
throw new Exception('Not an valid FFMpeg file "' . $inPath . '"');
|
||||
}
|
||||
|
||||
//TODO Can you retranscode?
|
||||
$queries = [
|
||||
new Query('bucketId', Query::TYPE_EQUAL, [$this->args['bucketId']]),
|
||||
new Query('fileId', Query::TYPE_EQUAL, [$this->args['fileId']])
|
||||
];
|
||||
|
||||
$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()));
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$sourceInfo = $this->getVideoInfo($ffprobe->streams($inPath));
|
||||
$video = $ffmpeg->open($inPath);
|
||||
|
||||
foreach (Config::getParam('renditions', []) as $rendition) {
|
||||
foreach (['hls', 'dash'] as $stream) {
|
||||
$query = Authorization::skip(function () use ($collection, $rendition, $stream) {
|
||||
return $this->database->createDocument($collection, new Document([
|
||||
'bucketId' => $this->args['bucketId'],
|
||||
'fileId' => $this->args['fileId'],
|
||||
'renditionId' => $rendition['id'],
|
||||
'renditionName' => $rendition['name'],
|
||||
'startedAt' => time(),
|
||||
'status' => 'started',
|
||||
'stream' => $stream,
|
||||
]));
|
||||
});
|
||||
|
||||
try {
|
||||
$representation = (new Representation())->
|
||||
setKiloBitrate($rendition['videoBitrate'])->
|
||||
setAudioKiloBitrate($rendition['audioBitrate'])->
|
||||
setResize($rendition['width'], $rendition['height']);
|
||||
|
||||
$format = new Streaming\Format\X264();
|
||||
$format->on('progress', function ($video, $format, $percentage) use ($query, $collection) {
|
||||
if ($percentage % 3 === 0) {
|
||||
$query->setAttribute('progress', (string)$percentage);
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
$metadata = $this->transcode($stream, $video, $format, $representation);
|
||||
|
||||
if (!empty($metadata)) {
|
||||
$query->setAttribute('metadata', json_encode($metadata));
|
||||
}
|
||||
|
||||
$query->setAttribute('status', 'ended');
|
||||
$query->setAttribute('endedAt', time());
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
|
||||
/** Upload & remove files **/
|
||||
$start = 0;
|
||||
$fileNames = scandir($this->outDir);
|
||||
|
||||
foreach ($fileNames as $fileName) {
|
||||
if (
|
||||
$fileName === '.' ||
|
||||
$fileName === '..' ||
|
||||
str_contains($fileName, '.json')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deviceFiles = $this->getVideoDevice($project->getId());
|
||||
$devicePath = $deviceFiles->getPath($this->args['fileId']);
|
||||
$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 . $rendition['name'] . DIRECTORY_SEPARATOR . $fileName, $data, \mime_content_type($this->outDir . $fileName));
|
||||
|
||||
if ($start === 0) {
|
||||
$query->setAttribute('status', 'uploading');
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
$start = 1;
|
||||
}
|
||||
|
||||
//$metadata=[];
|
||||
//$chunksUploaded = $deviceFiles->upload($file, $path, -1, 1, $metadata);
|
||||
//var_dump($chunksUploaded);
|
||||
// if (empty($chunksUploaded)) {
|
||||
// throw new Exception('Failed uploading file', 500, Exception::GENERAL_SERVER_ERROR);
|
||||
//}
|
||||
// }
|
||||
|
||||
@unlink($this->outDir . $fileName);
|
||||
}
|
||||
|
||||
$query->setAttribute('status', 'ready');
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
} catch (\Throwable $th) {
|
||||
$query->setAttribute('metadata', json_encode([
|
||||
'code' => $th->getCode(),
|
||||
'message' => $th->getMessage(),
|
||||
]));
|
||||
|
||||
$query->setAttribute('status', 'error');
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $metadata array
|
||||
* @return array
|
||||
*/
|
||||
private function getMetadataExport(array $metadata): array
|
||||
{
|
||||
$info = [];
|
||||
|
||||
if (!empty($metadata['stream']['resolutions'][0])) {
|
||||
$general = $metadata['stream']['resolutions'][0];
|
||||
$info['resolution'] = $general['dimension'];
|
||||
}
|
||||
|
||||
if (!empty($metadata['video']['streams'])) {
|
||||
foreach ($metadata['video']['streams'] as $streams) {
|
||||
if ($streams['codec_type'] === 'video') {
|
||||
$info['duration'] = $streams['duration'];
|
||||
$info['video']['codec'] = $streams['codec_name'] . ',' . $streams['codec_tag_string'];
|
||||
$info['video']['bitRate'] = $streams['bit_rate'];
|
||||
$info['video']['frameRate'] = $streams['avg_frame_rate'];
|
||||
} elseif ($streams['codec_type'] === 'audio') {
|
||||
$info['audio']['codec'] = $streams['codec_name'] . ',' . $streams['codec_tag_string'];
|
||||
$info['audio']['bitRate'] = $streams['sample_rate'];
|
||||
$info['audio']['samplRate'] = $streams['bit_rate'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stream
|
||||
* @param $video Media
|
||||
* @param $format StreamFormat
|
||||
* @param $representation Representation
|
||||
* @return array
|
||||
*/
|
||||
private function transcode(string $stream, Media $video, StreamFormat $format, Representation $representation): string | array
|
||||
{
|
||||
|
||||
$additionalParams = [
|
||||
'-sn',
|
||||
'-vf', 'scale=iw:-2:force_original_aspect_ratio=increase,setsar=1:1'
|
||||
];
|
||||
|
||||
$segementSize = 6;
|
||||
|
||||
if ($stream === 'dash') {
|
||||
$dash = $video->dash()
|
||||
->setFormat($format)
|
||||
->setSegDuration($segementSize)
|
||||
->addRepresentation($representation)
|
||||
->setAdditionalParams($additionalParams)
|
||||
->save($this->outPath);
|
||||
|
||||
$xml = simplexml_load_string(
|
||||
file_get_contents($this->outDir . $this->args['fileId'] . '.mpd')
|
||||
);
|
||||
|
||||
return [
|
||||
'general' => $this->getMetadataExport($dash->metadata()->export()),
|
||||
'dash' => !empty($xml) ? json_decode(json_encode((array)$xml), true) : [],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
$hls = $video->hls()
|
||||
->setFormat($format)
|
||||
->setHlsTime($segementSize)
|
||||
->addRepresentation($representation)
|
||||
->setAdditionalParams($additionalParams)
|
||||
->setHlsBaseUrl(self::HLS_BASE_URL)
|
||||
->save($this->outPath);
|
||||
|
||||
return ['general' => $this->getMetadataExport($hls->metadata()->export())];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $streams StreamCollection
|
||||
* @return array
|
||||
*/
|
||||
private function getVideoInfo(StreamCollection $streams): array
|
||||
{
|
||||
return [
|
||||
'duration' => $streams->videos()->first()->get('duration'),
|
||||
'height' => $streams->videos()->first()->get('height'),
|
||||
'width' => $streams->videos()->first()->get('width'),
|
||||
'frameRate' => $streams->videos()->first()->get('r_frame_rate'),
|
||||
'bitrateKb' => $streams->videos()->first()->get('bit_rate') / 1000,
|
||||
'bitrateMb' => $streams->videos()->first()->get('bit_rate') / 1000 / 1000,
|
||||
];
|
||||
}
|
||||
|
||||
private function cleanup(): bool
|
||||
{
|
||||
return \exec("rm -rf {$this->basePath}");
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
$this->cleanup();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
|
||||
then
|
||||
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
else
|
||||
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
fi
|
||||
|
||||
INTERVAL=0.1 QUEUE='v1-transcoding' APP_INCLUDE='/usr/src/code/app/workers/transcoding.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
|
||||
+4
-2
@@ -51,7 +51,7 @@
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/domains": "1.1.*",
|
||||
"utopia-php/swoole": "0.3.*",
|
||||
"utopia-php/storage": "0.9.*",
|
||||
"utopia-php/storage": "dev-main",
|
||||
"utopia-php/websocket": "0.1.0",
|
||||
"utopia-php/image": "0.5.*",
|
||||
"utopia-php/orchestration": "0.4.*",
|
||||
@@ -63,7 +63,9 @@
|
||||
"chillerlan/php-qrcode": "4.3.3",
|
||||
"adhocore/jwt": "1.1.2",
|
||||
"slickdeals/statsd": "3.1.0",
|
||||
"squizlabs/php_codesniffer": "^3.6"
|
||||
"aminyazdanpanah/php-ffmpeg-video-streaming": "^1.2",
|
||||
"squizlabs/php_codesniffer": "^3.6",
|
||||
"ext-simplexml": "*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
||||
Generated
+1009
-116
File diff suppressed because it is too large
Load Diff
+68
-44
@@ -7,38 +7,14 @@ x-logging: &x-logging
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-file: '5'
|
||||
max-size: '10m'
|
||||
|
||||
x-env-storage: &x-env-storage |-
|
||||
_APP_STORAGE_DEVICE
|
||||
_APP_STORAGE_S3_ACCESS_KEY
|
||||
_APP_STORAGE_S3_SECRET
|
||||
_APP_STORAGE_S3_REGION
|
||||
_APP_STORAGE_S3_BUCKET
|
||||
_APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
_APP_STORAGE_DO_SPACES_SECRET
|
||||
_APP_STORAGE_DO_SPACES_REGION
|
||||
_APP_STORAGE_DO_SPACES_BUCKET
|
||||
_APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
_APP_STORAGE_BACKBLAZE_SECRET
|
||||
_APP_STORAGE_BACKBLAZE_REGION
|
||||
_APP_STORAGE_BACKBLAZE_BUCKET
|
||||
_APP_STORAGE_DO_SPACES_BUCKET
|
||||
_APP_STORAGE_LINODE_ACCESS_KEY
|
||||
_APP_STORAGE_LINODE_SECRET
|
||||
_APP_STORAGE_LINODE_REGION
|
||||
_APP_STORAGE_LINODE_BUCKET
|
||||
_APP_STORAGE_WASABI_ACCESS_KEY
|
||||
_APP_STORAGE_WASABI_SECRET
|
||||
_APP_STORAGE_WASABI_REGION
|
||||
_APP_STORAGE_WASABI_BUCKET
|
||||
max-file: "5"
|
||||
max-size: 10m
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:2.7
|
||||
image: traefik:2.5
|
||||
<<: *x-logging
|
||||
container_name: appwrite-traefik
|
||||
command:
|
||||
@@ -96,6 +72,7 @@ services:
|
||||
- traefik.http.routers.appwrite_api_https.tls=true
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-video:/storage/video:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
@@ -103,7 +80,7 @@ services:
|
||||
- ./phpunit.xml:/usr/src/code/phpunit.xml
|
||||
- ./tests:/usr/src/code/tests
|
||||
- ./app:/usr/src/code/app
|
||||
# - ./vendor:/usr/src/code/vendor
|
||||
- ./vendor:/usr/src/code/vendor #TODO remove when done!!
|
||||
- ./docs:/usr/src/code/docs
|
||||
- ./public:/usr/src/code/public
|
||||
- ./src:/usr/src/code/src
|
||||
@@ -152,11 +129,18 @@ services:
|
||||
- _APP_INFLUXDB_HOST
|
||||
- _APP_INFLUXDB_PORT
|
||||
- _APP_STORAGE_LIMIT
|
||||
- _APP_STORAGE_PREVIEW_LIMIT
|
||||
- _APP_STORAGE_ANTIVIRUS
|
||||
- _APP_STORAGE_ANTIVIRUS_HOST
|
||||
- _APP_STORAGE_ANTIVIRUS_PORT
|
||||
- *x-env-storage
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_FUNCTIONS_SIZE_LIMIT
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
@@ -166,7 +150,6 @@ services:
|
||||
- _APP_FUNCTIONS_MEMORY_SWAP
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_STATSD_HOST
|
||||
@@ -225,6 +208,40 @@ services:
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-transcoding:
|
||||
entrypoint: worker-transcoding
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-transcoding
|
||||
build:
|
||||
context: .
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-video:/storage/video:rw
|
||||
- ./tests:/usr/src/code/tests #TODO remove when done!!
|
||||
- ./vendor:/usr/src/code/vendor #TODO remove when done!!
|
||||
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-audits:
|
||||
entrypoint: worker-audits
|
||||
<<: *x-logging
|
||||
@@ -293,6 +310,7 @@ services:
|
||||
- mariadb
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-video:/storage/video:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
@@ -311,11 +329,18 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- *x-env-storage
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
|
||||
appwrite-worker-database:
|
||||
entrypoint: worker-database
|
||||
@@ -365,7 +390,6 @@ services:
|
||||
- _APP_ENV
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -397,7 +421,6 @@ services:
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
- _APP_REDIS_HOST
|
||||
@@ -441,7 +464,6 @@ services:
|
||||
- _APP_DB_PASS
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_USAGE_STATS
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
@@ -488,7 +510,15 @@ services:
|
||||
- OPEN_RUNTIMES_NETWORK
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- *x-env-storage
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
|
||||
@@ -539,18 +569,11 @@ services:
|
||||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
@@ -779,6 +802,7 @@ volumes:
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
appwrite-uploads:
|
||||
appwrite-video:
|
||||
appwrite-certificates:
|
||||
appwrite-functions:
|
||||
appwrite-builds:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Resque;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Transcoding extends Event
|
||||
{
|
||||
protected string $bucketId = '';
|
||||
protected string $fileId = '';
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Event::TRANSCODING_QUEUE_NAME, Event::TRANSCODING_CLASS_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets bucketId event.
|
||||
*
|
||||
* @param $bucketId string
|
||||
* @return self
|
||||
*/
|
||||
public function setBucketId(string $bucketId): self
|
||||
{
|
||||
$this->bucketId = $bucketId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bucketId.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getBucketId(): ?string
|
||||
{
|
||||
return $this->bucketId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets fileId.
|
||||
*
|
||||
* @param $fileId string
|
||||
* @return self
|
||||
*/
|
||||
public function setFileId(string $fileId): self
|
||||
{
|
||||
$this->fileId = $fileId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns fileId.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getFileId(): ?string
|
||||
{
|
||||
return $this->fileId;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the function event and sends it to the functions worker.
|
||||
*
|
||||
* @return string|bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function trigger(): string|bool
|
||||
{
|
||||
return Resque::enqueue($this->queue, $this->class, [
|
||||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'bucketId' => $this->bucketId,
|
||||
'fileId' => $this->fileId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ use Appwrite\Utopia\Response\Model\UsageFunctions;
|
||||
use Appwrite\Utopia\Response\Model\UsageProject;
|
||||
use Appwrite\Utopia\Response\Model\UsageStorage;
|
||||
use Appwrite\Utopia\Response\Model\UsageUsers;
|
||||
use Appwrite\Utopia\Response\Model\FileRendition;
|
||||
|
||||
/**
|
||||
* @method Response setStatusCode(int $code = 200)
|
||||
@@ -127,6 +128,8 @@ class Response extends SwooleResponse
|
||||
public const MODEL_FILE_LIST = 'fileList';
|
||||
public const MODEL_BUCKET = 'bucket';
|
||||
public const MODEL_BUCKET_LIST = 'bucketList';
|
||||
public const MODEL_FILE_RENDITION = 'fileRendition';
|
||||
public const MODEL_FILE_RENDITIONS_LIST = 'fileRenditionsList';
|
||||
|
||||
// Locale
|
||||
public const MODEL_LOCALE = 'locale';
|
||||
@@ -218,6 +221,7 @@ 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))
|
||||
@@ -290,6 +294,8 @@ class Response extends SwooleResponse
|
||||
->setModel(new UsageBuckets())
|
||||
->setModel(new UsageFunctions())
|
||||
->setModel(new UsageProject())
|
||||
->setModel(new FileRendition())
|
||||
|
||||
// Verification
|
||||
// Recovery
|
||||
// Tests (keep last)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class FileRendition 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('renditionId', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Rendition ID.',
|
||||
'default' => '',
|
||||
'example' => 3,
|
||||
])
|
||||
->addRule('renditionName', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Rendition name.',
|
||||
'default' => '',
|
||||
'example' => '720P',
|
||||
])
|
||||
->addRule('startedAt', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Transcoding time started in Unix timestamp.',
|
||||
'default' => 0,
|
||||
'example' => 1592981220,
|
||||
])
|
||||
->addRule('endedAt', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Transcoding time ended in Unix timestamp.',
|
||||
'default' => 0,
|
||||
'example' => 1592981290,
|
||||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Rendition transcoding status',
|
||||
'default' => '',
|
||||
'example' => 'ready',
|
||||
])
|
||||
->addRule('progress', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Rendition trascoding progress',
|
||||
'default' => 0,
|
||||
'example' => 88,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'File rendition';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_FILE_RENDITION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Storage;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
|
||||
class VideoCustomServerTest extends Scope
|
||||
{
|
||||
use StorageBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
|
||||
public function testTranscoding(): array
|
||||
{
|
||||
|
||||
|
||||
// $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
|
||||
// 'content-type' => 'application/json',
|
||||
// 'x-appwrite-project' => $this->getProject()['$id'],
|
||||
// 'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
// ], [
|
||||
// 'bucketId' => 'unique()',
|
||||
// 'name' => 'Test Bucket 2',
|
||||
// 'permission' => 'file',
|
||||
// 'read' => ['role:all'],
|
||||
// 'write' => ['role:all']
|
||||
// ]);
|
||||
//
|
||||
// $source = __DIR__ . "/../../../resources/disk-a/large-file.mp4";
|
||||
// $totalSize = \filesize($source);
|
||||
// $chunkSize = 5 * 1024 * 1024;
|
||||
// $handle = @fopen($source, "rb");
|
||||
// $fileId = 'unique()';
|
||||
// $mimeType = mime_content_type($source);
|
||||
// $counter = 0;
|
||||
// $size = filesize($source);
|
||||
// $headers = [
|
||||
// 'content-type' => 'multipart/form-data',
|
||||
// 'x-appwrite-project' => $this->getProject()['$id']
|
||||
// ];
|
||||
// $id = '';
|
||||
//
|
||||
// while (!feof($handle)) {
|
||||
// $curlFile = new \CURLFile('data:' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'in1.mp4');
|
||||
// $headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size) . '/' . $size;
|
||||
//
|
||||
// if (!empty($id)) {
|
||||
// $headers['x-appwrite-id'] = $id;
|
||||
// }
|
||||
//
|
||||
// $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket['body']['$id'] . '/files', array_merge($headers, $this->getHeaders()), [
|
||||
// 'fileId' => $fileId,
|
||||
// 'file' => $curlFile,
|
||||
// 'read' => ['role:all'],
|
||||
// 'write' => ['role:all'],
|
||||
// ]);
|
||||
// $counter++;
|
||||
// $id = $file['body']['$id'];
|
||||
// }
|
||||
// @fclose($handle);
|
||||
//
|
||||
// $pid = $this->getProject()['$id'];
|
||||
// $key = $this->getProject()['apiKey'];
|
||||
// $fid = $id;
|
||||
// $bid = $bucket['body']['$id'];
|
||||
//
|
||||
// var_dump($pid);
|
||||
// var_dump($key);
|
||||
// var_dump($fid);
|
||||
// var_dump($bid);
|
||||
|
||||
|
||||
$pid = '62aaf3408decb0f5a0b3';
|
||||
$key = '6e8bf2fd07e5206a9b90efbc3dfbf0794a8e35810838e6b906f0c651a510924b8cf0c535b3a4e84b0330344153a4d8d5e413a0d9314f6955a4b2693633ab120d4f674dd668820d4c195d3006bd814003de18dc2161d7ce639a03cd37fd6fa14151445eddb5c9a294ddf16276ec97d56ecb7275eddab3517254bc7201688d8f47';
|
||||
$fid = '62aaf3423b91f5660b04';
|
||||
$bid = '62aaf340e9c06064668d';
|
||||
|
||||
$transcoding = $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']
|
||||
]);
|
||||
|
||||
return [
|
||||
'projectId' => $pid,
|
||||
'apiKey' => $key,
|
||||
'bucketId' => $bid,
|
||||
'fileId' => $fid,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function testRenditions(): void
|
||||
{
|
||||
|
||||
$pid = '62aaf3408decb0f5a0b3';
|
||||
$key = '6e8bf2fd07e5206a9b90efbc3dfbf0794a8e35810838e6b906f0c651a510924b8cf0c535b3a4e84b0330344153a4d8d5e413a0d9314f6955a4b2693633ab120d4f674dd668820d4c195d3006bd814003de18dc2161d7ce639a03cd37fd6fa14151445eddb5c9a294ddf16276ec97d56ecb7275eddab3517254bc7201688d8f47';
|
||||
$fid = '62aaf3423b91f5660b04';
|
||||
$bid = '62aaf340e9c06064668d';
|
||||
|
||||
|
||||
$renditions = $this->client->call(Client::METHOD_GET, '/video/buckets/' . $bid . '/files/' . $fid . '/renditions', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $pid,
|
||||
'x-appwrite-key' => $key,
|
||||
]);
|
||||
var_dump($renditions['body']);
|
||||
}
|
||||
|
||||
|
||||
public function testPlaylist(): void
|
||||
{
|
||||
|
||||
$pid = '62aaf3408decb0f5a0b3';
|
||||
$key = '6e8bf2fd07e5206a9b90efbc3dfbf0794a8e35810838e6b906f0c651a510924b8cf0c535b3a4e84b0330344153a4d8d5e413a0d9314f6955a4b2693633ab120d4f674dd668820d4c195d3006bd814003de18dc2161d7ce639a03cd37fd6fa14151445eddb5c9a294ddf16276ec97d56ecb7275eddab3517254bc7201688d8f47';
|
||||
$fid = '62aaf3423b91f5660b04';
|
||||
$bid = '62aaf340e9c06064668d';
|
||||
$stream = 'dash';
|
||||
|
||||
$renditions = $this->client->call(Client::METHOD_GET, '/video/buckets/' . $bid . '/files/' . $stream . '/' . $fid, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $pid,
|
||||
'x-appwrite-key' => $key,
|
||||
]);
|
||||
var_dump($renditions['body']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user