This commit is contained in:
shimon
2023-04-02 13:48:59 +03:00
parent 4a8ef126bd
commit bccd1a96c0
11 changed files with 463 additions and 104 deletions
+22
View File
@@ -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,
+5
View File
@@ -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',
+1 -1
View File
@@ -1112,4 +1112,4 @@ return [
"name" => "Chinese",
"nativeName" => "中文"
],
];
];
+220 -12
View File
@@ -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
View File
@@ -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;
+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
+45 -11
View File
@@ -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);
}
}
+1
View File
@@ -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;