mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
timeline
This commit is contained in:
@@ -3575,6 +3575,28 @@ $collections = [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'timeline',
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'preview',
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'size',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
|
||||
@@ -584,6 +584,11 @@ return [
|
||||
'description' => 'Video not found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::VIDEO_TIMELINE_NOT_FOUND => [
|
||||
'name' => Exception::VIDEO_TIMELINE_NOT_FOUND,
|
||||
'description' => 'Video timeline not found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND => [
|
||||
'name' => Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND,
|
||||
'description' => 'Video rendition segment not found',
|
||||
|
||||
@@ -1112,4 +1112,4 @@ return [
|
||||
"name" => "Chinese",
|
||||
"nativeName" => "中文"
|
||||
],
|
||||
];
|
||||
];
|
||||
|
||||
+220
-12
@@ -2,6 +2,7 @@
|
||||
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Transcoding;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Files;
|
||||
use Appwrite\Utopia\Response;
|
||||
@@ -14,8 +15,12 @@ use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Storage\Compression\Algorithms\GZIP;
|
||||
use Utopia\Storage\Compression\Algorithms\Zstd;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\HexColor;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -79,9 +84,10 @@ App::post('/v1/videos')
|
||||
->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.')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->action(action: function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode) {
|
||||
->action(action: function (string $bucketId, string $fileId, Request $request, Response $response, Document $project, Database $dbForProject, string $mode) {
|
||||
|
||||
$file = validateFilePermissions($dbForProject, $bucketId, $fileId, $mode);
|
||||
|
||||
@@ -101,6 +107,18 @@ App::post('/v1/videos')
|
||||
]));
|
||||
});
|
||||
|
||||
(new Transcoding())
|
||||
->setAction('preview')
|
||||
->setProject($project)
|
||||
->setVideo($video)
|
||||
->trigger();
|
||||
|
||||
(new Transcoding())
|
||||
->setAction('timeline')
|
||||
->setProject($project)
|
||||
->setVideo($video)
|
||||
->trigger();
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($video, Response::MODEL_VIDEO);
|
||||
});
|
||||
@@ -268,6 +286,203 @@ App::get('/v1/videos')
|
||||
]), Response::MODEL_VIDEO_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/videos/timeline/:videoId')
|
||||
->desc('Get video timeline ')
|
||||
->groups(['api', 'videos'])
|
||||
->label('scope', 'videos.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'videos')
|
||||
->label('sdk.method', 'getTimeline')
|
||||
->label('sdk.description', '/docs/references/videos/get-video-timeline.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VIDEO)
|
||||
->param('videoId', '', new UID(), 'Video unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('deviceVideos')
|
||||
->inject('mode')
|
||||
->action(function (string $videoId, Response $response, Database $dbForProject, Device $deviceVideos, string $mode) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId));
|
||||
|
||||
if ($video->isEmpty()) {
|
||||
throw new Exception(Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode);
|
||||
|
||||
if (!$video->getAttribute('timeline')) {
|
||||
throw new Exception(Exception::VIDEO_TIMELINE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$data = $deviceVideos->read($deviceVideos->getPath($video->getId() . '/timeline') . '/timeline.vtt');
|
||||
$response->setContentType('text/vtt')
|
||||
->send($data);
|
||||
});
|
||||
|
||||
App::get('/v1/videos/:videoId/preview')
|
||||
->desc('Get video file preview')
|
||||
->groups(['api', 'videos'])
|
||||
->label('scope', 'videos.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'bucket/{request.bucketId}')
|
||||
->label('cache.resource', 'video/{request.videoId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'videos')
|
||||
->label('sdk.method', 'getVideoImagePreview')
|
||||
->label('sdk.description', '/docs/references/storage/get-video-image-preview.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE)
|
||||
->label('sdk.methodType', 'location')
|
||||
->param('videoId', '', new UID(), 'Video unique ID.') ->param('fileId', '', new UID(), 'File ID')
|
||||
->param('width', 0, new Range(0, 4000), 'Resize preview image width, Pass an integer between 0 to 4000.', true)
|
||||
->param('height', 0, new Range(0, 4000), 'Resize preview image height, Pass an integer between 0 to 4000.', true)
|
||||
->param('gravity', Image::GRAVITY_CENTER, new WhiteList(Image::getGravityTypes()), 'Image crop gravity. Can be one of ' . implode(",", Image::getGravityTypes()), true)
|
||||
->param('quality', 100, new Range(0, 100), 'Preview image quality. Pass an integer between 0 to 100. Defaults to 100.', true)
|
||||
->param('borderWidth', 0, new Range(0, 100), 'Preview image border in pixels. Pass an integer between 0 to 100. Defaults to 0.', true)
|
||||
->param('borderColor', '', new HexColor(), 'Preview image border color. Use a valid HEX color, no # is needed for prefix.', true)
|
||||
->param('borderRadius', 0, new Range(0, 4000), 'Preview image border radius in pixels. Pass an integer between 0 to 4000.', true)
|
||||
->param('opacity', 1, new Range(0, 1, Range::TYPE_FLOAT), 'Preview image opacity. Only works with images having an alpha channel (like png). Pass a number between 0 to 1.', true)
|
||||
->param('rotation', 0, new Range(-360, 360), 'Preview image rotation in degrees. Pass an integer between -360 and 360.', true)
|
||||
->param('background', '', new HexColor(), 'Preview image background color. Only works with transparent images (png). Use a valid HEX color, no # is needed for prefix.', true)
|
||||
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('deviceFiles')
|
||||
->inject('deviceLocal')
|
||||
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceFiles, Device $deviceLocal) {
|
||||
|
||||
if (!\extension_loaded('imagick')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
}
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
|
||||
$validator = new Authorization(Database::PERMISSION_READ);
|
||||
$valid = $validator->isValid($bucket->getRead());
|
||||
if (!$fileSecurity && !$valid) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support
|
||||
$output = 'jpg';
|
||||
}
|
||||
|
||||
$inputs = Config::getParam('storage-inputs');
|
||||
$outputs = Config::getParam('storage-outputs');
|
||||
$fileLogos = Config::getParam('storage-logos');
|
||||
|
||||
if ($fileSecurity && !$valid) {
|
||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
|
||||
} else {
|
||||
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
|
||||
}
|
||||
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $file->getAttribute('path');
|
||||
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
|
||||
$algorithm = $file->getAttribute('algorithm', 'none');
|
||||
$cipher = $file->getAttribute('openSSLCipher');
|
||||
$mime = $file->getAttribute('mimeType');
|
||||
if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) {
|
||||
if (!\in_array($mime, $inputs)) {
|
||||
$path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default'];
|
||||
} else {
|
||||
// it was an image but the file size exceeded the limit
|
||||
$path = $fileLogos['default_image'];
|
||||
}
|
||||
|
||||
$algorithm = 'none';
|
||||
$cipher = null;
|
||||
$background = (empty($background)) ? 'eceff1' : $background;
|
||||
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
|
||||
$deviceFiles = $deviceLocal;
|
||||
}
|
||||
|
||||
if (!$deviceFiles->exists($path)) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (empty($output)) {
|
||||
// when file extension is not provided and the mime type is not one of our supported outputs
|
||||
// we fallback to `jpg` output format
|
||||
$output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type;
|
||||
}
|
||||
|
||||
|
||||
$source = $deviceFiles->read($path);
|
||||
|
||||
if (!empty($cipher)) { // Decrypt
|
||||
$source = OpenSSL::decrypt(
|
||||
$source,
|
||||
$file->getAttribute('openSSLCipher'),
|
||||
App::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
|
||||
0,
|
||||
\hex2bin($file->getAttribute('openSSLIV')),
|
||||
\hex2bin($file->getAttribute('openSSLTag'))
|
||||
);
|
||||
}
|
||||
|
||||
switch ($algorithm) {
|
||||
case 'zstd':
|
||||
$compressor = new Zstd();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
case 'gzip':
|
||||
$compressor = new GZIP();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
}
|
||||
|
||||
$image = new Image($source);
|
||||
|
||||
$image->crop((int) $width, (int) $height, $gravity);
|
||||
|
||||
if (!empty($opacity) || $opacity === 0) {
|
||||
$image->setOpacity($opacity);
|
||||
}
|
||||
|
||||
if (!empty($background)) {
|
||||
$image->setBackground('#' . $background);
|
||||
}
|
||||
|
||||
if (!empty($borderWidth)) {
|
||||
$image->setBorder($borderWidth, '#' . $borderColor);
|
||||
}
|
||||
|
||||
if (!empty($borderRadius)) {
|
||||
$image->setBorderRadius($borderRadius);
|
||||
}
|
||||
|
||||
if (!empty($rotation)) {
|
||||
$image->setRotation(($rotation + 360) % 360);
|
||||
}
|
||||
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType($contentType)
|
||||
->file($data)
|
||||
;
|
||||
|
||||
unset($image);
|
||||
});
|
||||
|
||||
App::post('/v1/videos/:videoId/subtitles')
|
||||
->desc('Add subtitle to video')
|
||||
->groups(['api', 'videos'])
|
||||
@@ -285,7 +500,7 @@ App::post('/v1/videos/:videoId/subtitles')
|
||||
->param('bucketId', '', new CustomId(), 'Subtitle bucket unique ID.')
|
||||
->param('fileId', '', new CustomId(), 'Subtitle file unique ID.')
|
||||
->param('name', '', new Text(32), 'Subtitle name.')
|
||||
->param('code', '', new Text(3), 'Subtitle ISO 639-2 three letters alpha code.')
|
||||
->param('code', '', new WhiteList(\array_column(Config::getParam('locale-languages'), 'code2')), 'Subtitle ISO 639-2 3 letters alpha code.')
|
||||
->param('default', false, new Boolean(true), 'Default subtitle.')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
@@ -293,14 +508,6 @@ 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, string $mode) {
|
||||
|
||||
$code = strtolower($code);
|
||||
$languages = Config::getParam('locale-languages');
|
||||
$found = array_search($code, array_column($languages, 'code2'));
|
||||
|
||||
if (!$found) {
|
||||
throw new Exception(Exception::VIDEO_LANGUAGE_CODE_NOT_VALID);
|
||||
}
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId));
|
||||
|
||||
if ($video->isEmpty()) {
|
||||
@@ -490,10 +697,11 @@ App::post('/v1/videos/:videoId/rendition')
|
||||
|
||||
$transcoder = new Transcoding();
|
||||
$transcoder
|
||||
->setOutput($output)
|
||||
->setAction('encode')
|
||||
->setProject($project)
|
||||
->setVideo($video)
|
||||
->setProfile($profile)
|
||||
->setOutput($output)
|
||||
->trigger();
|
||||
|
||||
$response->noContent();
|
||||
@@ -817,7 +1025,7 @@ App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/streams/:s
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('output', '', new WhiteList(['hls']), 'Output name.')
|
||||
->param('renditionId', null, new UID(), 'Rendition unique ID.')
|
||||
->param('streamId', '', new Range(0, 10), 'Stream ID.')
|
||||
->param('streamId', 0, new Range(0, 10), 'Stream ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
|
||||
+1
-1
@@ -889,7 +889,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
|
||||
/** @var Utopia\Database\Document $console */
|
||||
|
||||
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console'));
|
||||
//$projectId = '6408a2e6c8b85007caa1';
|
||||
$projectId = '6425c03a3ce2f21b2d1d';
|
||||
|
||||
if ($projectId === 'console') {
|
||||
return $console;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
<?php foreach ($this->getParam('audios', []) as $audio): ?>
|
||||
<?php foreach ($this->getParam('audios', []) as $audio):?>
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_audio"<?php if($audio['name'] !== null):?>,NAME="<?php echo $audio['name'];?>"<?php endif;?>,DEFAULT=<?php echo $audio['default']; ?><?php if($audio['language'] !== null):?>,LANGUAGE="<?php echo $audio['language'];?>"<?php endif;?><?php if($audio['uri'] !== null):?>,URI="<?php echo $audio['uri'];?>"<?php endif;?><?php echo PHP_EOL?>
|
||||
<?php endforeach; ?>
|
||||
<?php foreach ($this->getParam('subtitles', []) as $subtitle): ?>
|
||||
<?php endforeach;?>
|
||||
<?php foreach ($this->getParam('subtitles', []) as $subtitle):?>
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="<?php echo $this->escape($subtitle['name']); ?>",DEFAULT=<?php echo $subtitle['default']; ?>,AUTOSELECT=NO,FORCED=NO,LANGUAGE="<?php echo $this->escape($subtitle['code']); ?>",URI="<?php echo $subtitle['uri']; ?>"<?php echo PHP_EOL?>
|
||||
<?php endforeach; ?>
|
||||
<?php foreach ($this->getParam('renditions', []) as $rendition): ?>
|
||||
<?php endforeach;?>
|
||||
<?php foreach ($this->getParam('renditions', []) as $rendition):?>
|
||||
#EXT-X-STREAM-INF:BANDWIDTH="<?php echo $rendition['bandwidth']; ?>",RESOLUTION="<?php echo $rendition['resolution']; ?>",NAME="<?php echo $rendition['name']; ?>"<?php if($rendition['subs'] !== null):?>,SUBTITLES="<?php echo $rendition['subs'];?>"<?php endif;?><?php if($rendition['audio'] !== null):?>,AUDIO="<?php echo $rendition['audio'];?>"<?php endif;?> <?php echo PHP_EOL?>
|
||||
<?php echo $rendition['uri'] ?><?php echo PHP_EOL?>
|
||||
<?php endforeach; ?>
|
||||
<?php echo $rendition['uri'];?><?php echo PHP_EOL?>
|
||||
<?php endforeach;?>
|
||||
+160
-70
@@ -57,6 +57,8 @@ class TranscodingV1 extends Worker
|
||||
|
||||
private Document $video;
|
||||
|
||||
private string $action;
|
||||
|
||||
private Document $profile;
|
||||
|
||||
private Document $project;
|
||||
@@ -76,12 +78,13 @@ class TranscodingV1 extends Worker
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$this->video = new Document($this->args['video']);
|
||||
$this->profile = new Document($this->args['profile']);
|
||||
$this->project = new Document($this->args['project']);
|
||||
$this->basePath .= $this->video->getId() . '/' . $this->profile->getId();
|
||||
$this->inDir = $this->basePath . '/in/';
|
||||
$this->outDir = $this->basePath . '/out/';
|
||||
$this->video = new Document($this->args['video'] ?? []);
|
||||
$this->profile = new Document($this->args['profile'] ?? []);
|
||||
$this->project = new Document($this->args['project'] ?? []);
|
||||
$this->action = $this->args['action'];
|
||||
$this->basePath .= uniqid();
|
||||
$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->video->getId();
|
||||
@@ -125,48 +128,140 @@ class TranscodingV1 extends Worker
|
||||
console::error('Not an valid Video file "' . $inPath . '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Original asset media info
|
||||
*/
|
||||
$mediaInfo = new MediaInfo();
|
||||
$mediaInfoContainer = $mediaInfo->getInfo($inPath);
|
||||
$general = $mediaInfoContainer->getGeneral();
|
||||
$this->video
|
||||
->setAttribute('duration', $general->has('duration') ? strval($general->get('duration')->getMilliseconds()) : '')
|
||||
->setAttribute('format', $general->has('format') ? $general->get('format')->getShortName() : '');
|
||||
|
||||
foreach ($mediaInfoContainer->getVideos() ?? [] as $video) {
|
||||
$this->video
|
||||
->setAttribute('height', $video->has('height') ? $video->get('height')->getAbsoluteValue() : 0)
|
||||
->setAttribute('width', $video->has('width') ? $video->get('width')->getAbsoluteValue() : 0)
|
||||
->setAttribute('aspectRatio', $video->has('display_aspect_ratio') ? $video->get('display_aspect_ratio')->getTextValue() : '')
|
||||
->setAttribute('videoFormat', $video->has('format') ? $video->get('format')->getShortName() : '')
|
||||
->setAttribute('videoFormatProfile', $video->has('format_profile') ? $video->get('format_profile') : '')
|
||||
->setAttribute('videoFrameRate', $video->has('frame_rate') ? strval($video->get('frame_rate')->getAbsoluteValue()) : '')
|
||||
->setAttribute('videoFrameRateMode', $video->has('frame_rate_mode') ? $video->get('frame_rate_mode')->getFullName() : '')
|
||||
->setAttribute('videoBitRate', $video->has('bit_rate') ? strval($video->get('bit_rate')->getAbsoluteValue()) : '');
|
||||
}
|
||||
|
||||
foreach ($mediaInfoContainer->getAudios() ?? [] as $audio) {
|
||||
$this->video
|
||||
->setAttribute('audioFormat', $audio->has('format') ? strval($audio->get('format')->getShortName()) : '')
|
||||
->setAttribute('audioSampleRate', $audio->has('sampling_rate') ? strval($audio->get('sampling_rate')->getAbsoluteValue()) : '')
|
||||
->setAttribute('audioBitRate', $audio->has('bit_rate') ? strval($audio->get('bit_rate')->getAbsoluteValue()) : '');
|
||||
}
|
||||
|
||||
console::info('Input video id: ' . $this->video->getId() . PHP_EOL .
|
||||
'Input name: ' . $this->file->getAttribute('name') . PHP_EOL .
|
||||
'Input width: ' . $this->video->getAttribute('width') . ' px' . PHP_EOL .
|
||||
'Input height: ' . $this->video->getAttribute('height') . ' px' . PHP_EOL .
|
||||
'Input duration: ' . ($this->video->getAttribute('duration') / 1000) . ' sec' . PHP_EOL .
|
||||
'Input size: ' . ($this->video->getAttribute('size') / 1024 / 1024) . ' MiB' . PHP_EOL .
|
||||
'Input videoBitRate: ' . ($this->video->getAttribute('videoBitRate') / 1000) . ' kb/s' . PHP_EOL .
|
||||
'Input audioBitRate: ' . ($this->video->getAttribute('audioBitRate') / 1000) . ' kb/s' . PHP_EOL);
|
||||
|
||||
$this->database->updateDocument('videos', $this->video->getId(), $this->video);
|
||||
|
||||
$media = $this->ffmpeg->open($inPath);
|
||||
|
||||
if ($this->action === 'timeline') {
|
||||
/**
|
||||
* Original asset metadata
|
||||
*/
|
||||
$mediaInfo = new MediaInfo();
|
||||
$mediaInfoContainer = $mediaInfo->getInfo($inPath);
|
||||
$general = $mediaInfoContainer->getGeneral();
|
||||
$this->video
|
||||
->setAttribute('duration', $general->has('duration') ? strval($general->get('duration')->getMilliseconds()) : '')
|
||||
->setAttribute('format', $general->has('format') ? $general->get('format')->getShortName() : '');
|
||||
|
||||
foreach ($mediaInfoContainer->getVideos() ?? [] as $video) {
|
||||
$this->video
|
||||
->setAttribute('height', $video->has('height') ? $video->get('height')->getAbsoluteValue() : 0)
|
||||
->setAttribute('width', $video->has('width') ? $video->get('width')->getAbsoluteValue() : 0)
|
||||
->setAttribute('aspectRatio', $video->has('display_aspect_ratio') ? $video->get('display_aspect_ratio')->getTextValue() : '')
|
||||
->setAttribute('videoFormat', $video->has('format') ? $video->get('format')->getShortName() : '')
|
||||
->setAttribute('videoFormatProfile', $video->has('format_profile') ? $video->get('format_profile') : '')
|
||||
->setAttribute('videoFrameRate', $video->has('frame_rate') ? strval($video->get('frame_rate')->getAbsoluteValue()) : '')
|
||||
->setAttribute('videoFrameRateMode', $video->has('frame_rate_mode') ? $video->get('frame_rate_mode')->getFullName() : '')
|
||||
->setAttribute('videoBitRate', $video->has('bit_rate') ? strval($video->get('bit_rate')->getAbsoluteValue()) : '');
|
||||
}
|
||||
|
||||
foreach ($mediaInfoContainer->getAudios() ?? [] as $audio) {
|
||||
$this->video
|
||||
->setAttribute('audioFormat', $audio->has('format') ? strval($audio->get('format')->getShortName()) : '')
|
||||
->setAttribute('audioSampleRate', $audio->has('sampling_rate') ? strval($audio->get('sampling_rate')->getAbsoluteValue()) : '')
|
||||
->setAttribute('audioBitRate', $audio->has('bit_rate') ? strval($audio->get('bit_rate')->getAbsoluteValue()) : '');
|
||||
}
|
||||
|
||||
console::info('Input video id: ' . $this->video->getId() . PHP_EOL .
|
||||
'Input name: ' . $this->file->getAttribute('name') . PHP_EOL .
|
||||
'Input width: ' . $this->video->getAttribute('width') . ' px' . PHP_EOL .
|
||||
'Input height: ' . $this->video->getAttribute('height') . ' px' . PHP_EOL .
|
||||
'Input duration: ' . ($this->video->getAttribute('duration') / 1000) . ' sec' . PHP_EOL .
|
||||
'Input size: ' . ($this->video->getAttribute('size') / 1024 / 1024) . ' MiB' . PHP_EOL .
|
||||
'Input videoBitRate: ' . ($this->video->getAttribute('videoBitRate') / 1000) . ' kb/s' . PHP_EOL .
|
||||
'Input audioBitRate: ' . ($this->video->getAttribute('audioBitRate') / 1000) . ' kb/s' . PHP_EOL);
|
||||
|
||||
$this->database->updateDocument('videos', $this->video->getId(), $this->video);
|
||||
|
||||
$interval = 2;
|
||||
$ranges = [
|
||||
['from' => 120, 'to' => 600, 'interval' => 5],
|
||||
['from' => 600, 'to' => 1800 , 'interval' => 10],
|
||||
['from' => 1800, 'to' => 3600, 'interval' => 20],
|
||||
['from' => 3600, 'to' => 99999, 'interval' => 30],
|
||||
];
|
||||
|
||||
foreach ($ranges as $range) {
|
||||
if (
|
||||
$this->video->getAttribute('duration') > $range['from'] &&
|
||||
$this->video->getAttribute('duration') <= $range['to']
|
||||
) {
|
||||
$interval = $range['interval'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$timeline['aspect'] = $this->video->getAttribute('width') / $this->video->getAttribute('height');
|
||||
$timeline['width'] = 160;
|
||||
$timeline['height'] = round($timeline['width'] / $timeline['aspect']);
|
||||
$timeline['size'] = '5x5';
|
||||
|
||||
$result = shell_exec("/usr/bin/ffmpeg -i " . $inPath . " -hide_banner -loglevel error -vsync vfr -vf 'select=isnan(prev_selected_t)+gte(t-prev_selected_t\," . $interval . "),scale=" . $timeline['width'] . ":" . $timeline['height'] . ",tile=" . $timeline['size'] . "' -qscale:v 3 " . $this->outDir . "img%d.jpg");
|
||||
|
||||
if ($result !== false) {
|
||||
$size = explode('x', $timeline['size']);
|
||||
$counter = 0;
|
||||
$images = ceil(($this->video->getAttribute('duration') / $interval) / ($size[0] * $size[1]));
|
||||
$data = "WEBVTT";
|
||||
for ($image = 1; $image <= $images; $image++) {
|
||||
for ($col = 0; $col < $size[0]; $col++) {
|
||||
for ($row = 0; $row < $size[1]; $row++) {
|
||||
$data .= "\n" . gmdate("H:i:s", $counter * $interval) . " --> " . gmdate("H:i:s", ($counter + 1) * $interval) . "\n" . "/img" . $image . ".png#xywh=" . ($row * $timeline['width']) . "," . ($col * $timeline['height']) . "," . $timeline['width'] . "," . $timeline['height'];
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($counter > 0) {
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/timeline/' . 'timeline.vtt',
|
||||
$data
|
||||
);
|
||||
|
||||
console::info('Uploading timeline vtt');
|
||||
|
||||
/** Upload**/
|
||||
$dir = new DirectoryIterator($this->outDir);
|
||||
foreach ($dir as $fileinfo) {
|
||||
if (!$fileinfo->isDot()) {
|
||||
console::info('Uploading ' . $fileinfo->getFilename());
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$this->getVideoDevice($this->project->getId())->getPath($this->video->getId() . '/timeline') . '/' . $fileinfo->getFilename(),
|
||||
(new Local('/'))->read($this->outDir . $fileinfo->getFilename())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->database->updateDocument('videos', $this->video->getId(), $this->video->setAttribute('timeline', true));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->action === 'preview') {
|
||||
$media
|
||||
->filters()
|
||||
->resize(new \FFMpeg\Coordinate\Dimension(640, 480))
|
||||
->synchronize();
|
||||
$media
|
||||
->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds(5))
|
||||
->save($this->outDir . ' 640x480.png');
|
||||
|
||||
/** Upload**/
|
||||
$dir = new DirectoryIterator($this->outDir);
|
||||
foreach ($dir as $fileinfo) {
|
||||
if (!$fileinfo->isDot()) {
|
||||
console::info('Uploading ' . $fileinfo->getFilename());
|
||||
$this->getVideoDevice($this->project->getId())->write(
|
||||
$this->getVideoDevice($this->project->getId())->getPath($this->video->getId() . '/preview') . '/' . $fileinfo->getFilename(),
|
||||
(new Local('/'))->read($this->outDir . $fileinfo->getFilename())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->database->updateDocument('videos', $this->video->getId(), $this->video->setAttribute('preview', true));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subs = [];
|
||||
$subtitles = $this->database->find('videos_subtitles', [
|
||||
Query::equal('videoId', [$this->video->getId()]),
|
||||
@@ -311,33 +406,29 @@ class TranscodingV1 extends Worker
|
||||
|
||||
console::info('Rendition ' . $query->getId() . ' conversion, done');
|
||||
|
||||
/** Upload & cleanup **/
|
||||
$start = 0;
|
||||
$fileNames = scandir($this->outDir);
|
||||
foreach ($fileNames as $fileName) {
|
||||
if ($fileName === '.' || $fileName === '..') {
|
||||
//str_contains($fileName, '.json')) {
|
||||
continue;
|
||||
}
|
||||
/** Upload**/
|
||||
$dir = new DirectoryIterator($this->outDir);
|
||||
foreach ($dir as $fileinfo) {
|
||||
if (!$fileinfo->isDot()) {
|
||||
console::info('Uploading ' . $fileinfo->getFilename());
|
||||
|
||||
$data = (new Local('/'))->read($this->outDir . $fileName);
|
||||
$to = $renditionPath;
|
||||
if (str_contains($fileName, "_subtitles_") || str_contains($fileName, ".vtt")) {
|
||||
$to = $renditionRootPath;
|
||||
}
|
||||
$data = (new Local('/'))->read($this->outDir . $fileinfo->getFilename());
|
||||
$to = $renditionPath;
|
||||
if (str_contains($fileinfo->getFilename(), "_subtitles_") || str_contains($fileinfo->getFilename(), ".vtt")) {
|
||||
$to = $renditionRootPath;
|
||||
}
|
||||
|
||||
$this->getVideoDevice($this->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);
|
||||
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
|
||||
$this->send($query, 'update');
|
||||
$start = 1;
|
||||
$this->getVideoDevice($this->project->getId())->write($to . $fileinfo->getFilename(), $data);
|
||||
|
||||
console::info('Uploading to [' . $to . ']');
|
||||
if ($fileinfo->key() === 0) {
|
||||
$query->setAttribute('progress', '100');
|
||||
$query->setAttribute('status', self::STATUS_UPLOADING);
|
||||
$query->setAttribute('path', $renditionPath);
|
||||
$this->database->updateDocument('videos_renditions', $query->getId(), $query);
|
||||
$this->send($query, 'update');
|
||||
console::info('Uploading to [' . $to . ']');
|
||||
}
|
||||
}
|
||||
@unlink($this->outDir . $fileName);
|
||||
}
|
||||
|
||||
$query->setAttribute('status', self::STATUS_READY);
|
||||
@@ -376,7 +467,6 @@ class TranscodingV1 extends Worker
|
||||
'-dn',
|
||||
'-sn',
|
||||
'-vf', 'scale=iw:-2:force_original_aspect_ratio=increase,setsar=1:1',
|
||||
'-b_strategy', '1',
|
||||
'-bf', '3',
|
||||
'-force_key_frames', 'expr:gte(t,n_forced*2)' //enforce strict key frame
|
||||
];
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ services:
|
||||
- ./src:/usr/src/code/src
|
||||
- ./dev:/usr/local/dev
|
||||
- ./tests/tmp:/tmp
|
||||
#- ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
|
||||
- ./vendor/utopia-php/framework:/usr/src/code/vendor/utopia-php/framework
|
||||
depends_on:
|
||||
- mariadb
|
||||
- redis
|
||||
|
||||
@@ -7,11 +7,10 @@ use Utopia\Database\Document;
|
||||
|
||||
class Transcoding extends Event
|
||||
{
|
||||
protected Document $video;
|
||||
|
||||
protected Document $profile;
|
||||
|
||||
protected string $output;
|
||||
protected ?Document $video = null;
|
||||
protected ?Document $profile = null;
|
||||
protected string $output = '';
|
||||
protected string $action = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -41,6 +40,28 @@ class Transcoding extends Event
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets action.
|
||||
*
|
||||
* @param string $action
|
||||
* @return self
|
||||
*/
|
||||
public function setAction(string $action): self
|
||||
{
|
||||
$this->action = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns action.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets video.
|
||||
@@ -96,12 +117,25 @@ class Transcoding extends Event
|
||||
*/
|
||||
public function trigger(): string|bool
|
||||
{
|
||||
return Resque::enqueue($this->queue, $this->class, [
|
||||
'output' => $this->output,
|
||||
$keys = [
|
||||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'video' => $this->video,
|
||||
'profile' => $this->profile,
|
||||
]);
|
||||
'user' => $this->user,
|
||||
'video' => $this->video,
|
||||
'action' => $this->action,
|
||||
];
|
||||
|
||||
if (!empty($this->getOutput())) {
|
||||
$keys = array_merge($keys, [
|
||||
'output' => $this->getOutput(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!empty($this->getProfile())) {
|
||||
$keys = array_merge($keys, [
|
||||
'profile' => $this->getProfile(),
|
||||
]);
|
||||
}
|
||||
|
||||
return Resque::enqueue($this->queue, $this->class, $keys);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ class Exception extends \Exception
|
||||
/** Video */
|
||||
public const VIDEO_PROFILE_NOT_FOUND = 'video_profile_not_found';
|
||||
public const VIDEO_NOT_VALID = 'video_not_valid';
|
||||
public const VIDEO_TIMELINE_NOT_FOUND = 'video_timeline_not_found';
|
||||
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';
|
||||
|
||||
@@ -336,7 +336,6 @@ class Realtime extends Adapter
|
||||
$channels[] = 'renditions.' . $payload->getId();
|
||||
$channels[] = 'videos.' . $payload->getAttribute('videoId');
|
||||
$roles = $payload->getRead();
|
||||
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user