diff --git a/.env b/.env index 992de4b7ae..567b9ceb40 100644 --- a/.env +++ b/.env @@ -23,7 +23,7 @@ _APP_DB_SCHEMA=appwrite _APP_DB_USER=user _APP_DB_PASS=password _APP_DB_ROOT_PASS=rootsecretpassword -_APP_STORAGE_DEVICE=Local +_APP_STORAGE_DEVICE=local _APP_STORAGE_S3_ACCESS_KEY= _APP_STORAGE_S3_SECRET= _APP_STORAGE_S3_REGION= @@ -31,7 +31,7 @@ _APP_STORAGE_S3_BUCKET= _APP_STORAGE_DO_SPACES_ACCESS_KEY= _APP_STORAGE_DO_SPACES_SECRET= _APP_STORAGE_DO_SPACES_REGION=fra1 -_APP_STORAGE_DO_SPACES_BUCKET= +_APP_STORAGE_DO_SPACES_BUCKET=videos-test _APP_STORAGE_BACKBLAZE_ACCESS_KEY= _APP_STORAGE_BACKBLAZE_SECRET= _APP_STORAGE_BACKBLAZE_REGION=us-west-004 diff --git a/app/config/collections.php b/app/config/collections.php index a797a36050..0bbf4a1e42 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -3219,6 +3219,17 @@ $collections = [ 'default' => null, 'filters' => [], ], + [ + '$id' => ID::custom('bucketInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('name'), 'type' => Database::VAR_STRING, @@ -3564,6 +3575,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'bucketInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'fileId', 'type' => Database::VAR_STRING, @@ -3575,6 +3597,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'fileInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'previewId', 'type' => Database::VAR_STRING, @@ -3586,6 +3619,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'previewInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'size', 'type' => Database::VAR_INTEGER, @@ -3767,6 +3811,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'videoInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'type', 'type' => Database::VAR_STRING, @@ -3833,6 +3888,17 @@ $collections = [ 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'videoInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, 'required' => false, 'default' => null, 'array' => false, @@ -3849,6 +3915,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'profileInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'name', 'type' => Database::VAR_STRING, @@ -4019,6 +4096,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'renditionInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'fileName', 'type' => Database::VAR_STRING, @@ -4167,7 +4255,18 @@ $collections = [ 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, - 'required' => false, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'videoInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, 'default' => null, 'array' => false, 'filters' => [], @@ -4183,6 +4282,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'bucketInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'fileId', 'type' => Database::VAR_STRING, @@ -4194,6 +4304,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'fileInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'path', 'type' => Database::VAR_STRING, @@ -4289,6 +4410,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'subtitleInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'fileName', 'type' => Database::VAR_STRING, diff --git a/app/config/videos-profiles.php b/app/config/videos-profiles.php index 7215fd0f7e..8a27496776 100644 --- a/app/config/videos-profiles.php +++ b/app/config/videos-profiles.php @@ -17,10 +17,24 @@ return [ 'height' => 576, ], [ - 'name' => '720p', + 'name' => '1080p', 'videoBitRate' => 3551, 'audioBitRate' => 128, 'width' => 1280, 'height' => 720, ], + [ + 'name' => '720p', + 'videoBitRate' => 4800, + 'audioBitRate' => 128, + 'width' => 1920, + 'height' => 1080, + ], + [ + 'name' => '2160p', + 'videoBitRate' => 16000, + 'audioBitRate' => 356, + 'width' => 4096, + 'height' => 2160, + ], ]; diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 271f2af6b3..9790a2a3a8 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -567,6 +567,7 @@ App::post('/v1/storage/buckets/:bucketId/files') '$id' => $fileId, '$permissions' => $permissions, 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getInternalId(), 'name' => $fileName, 'path' => $path, 'signature' => $fileHash, @@ -616,6 +617,7 @@ App::post('/v1/storage/buckets/:bucketId/files') '$id' => ID::custom($fileId), '$permissions' => $permissions, 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getInternalId(), 'name' => $fileName, 'path' => $path, 'signature' => '', diff --git a/app/controllers/api/videos.php b/app/controllers/api/videos.php index 84c207c603..f5747aba74 100644 --- a/app/controllers/api/videos.php +++ b/app/controllers/api/videos.php @@ -100,9 +100,11 @@ App::post('/v1/videos') $video = Authorization::skip(function () use ($dbForProject, $bucketId, $file) { return $dbForProject->createDocument('videos', new Document([ - 'bucketId' => $bucketId, - 'fileId' => $file->getId(), - 'size' => $file->getAttribute('sizeOriginal'), + 'bucketId' => $file->getAttribute('bucketId'), + 'bucketInternalId' => $file->getAttribute('bucketInternalId'), + 'fileId' => $file->getId(), + 'fileInternalId' => $file->getInternalId(), + 'size' => $file->getAttribute('sizeOriginal'), ])); }); @@ -191,10 +193,13 @@ App::put('/v1/videos/:videoId') $video = Authorization::skip(fn() => $dbForProject->updateDocument('videos', $videoId, new Document([ - 'bucketId' => $bucketId, - 'fileId' => $file->getId(), + 'bucketId' => $file->getAttribute('bucketId'), + 'bucketInternalId' => $file->getAttribute('bucketInternalId'), + 'fileId' => $file->getId(), + 'fileInternalId' => $file->getInternalId(), 'size' => $file->getAttribute('sizeOriginal'), 'previewId' => null, + 'previewInternalId' => null, 'duration' => null, 'width' => null, 'height' => null, @@ -480,9 +485,12 @@ App::post('/v1/videos/:videoId/subtitles') $subtitle = Authorization::skip(fn() => $dbForProject->createDocument('videos_subtitles', new Document([ - 'videoId' => $videoId, - 'bucketId' => $bucketId, - 'fileId' => $fileId, + 'videoId' => $video->getId(), + 'videoInternalId' => $video->getInternalId(), + 'bucketId' => $file->getAttribute('bucketId'), + 'bucketInternalId' => $file->getAttribute('bucketInternalId'), + 'fileId' => $file->getId(), + 'fileInternalId' => $file->getInternalId(), 'name' => $name, 'code' => $code, 'default' => $default, @@ -546,7 +554,22 @@ App::patch('/v1/videos/:videoId/subtitles/:subtitleId') ->param('default', false, new Boolean(true), 'Default subtitle.') ->inject('response') ->inject('dbForProject') - ->action(action: function (string $subtitleId, string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Response $response, Database $dbForProject) { + ->inject('mode') + ->action(action: function (string $subtitleId, string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Response $response, Database $dbForProject, 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); + + $file = validateFilePermissions($dbForProject, $bucketId, $fileId, $mode); + + if (!in_array($file->getAttribute('mimeType'), ['text/vtt','text/plain'])) { + throw new Exception(Exception::VIDEO_SUBTITLE_NOT_VALID); + } $code = strtolower($code); $languages = Config::getParam('locale-languages'); @@ -562,9 +585,12 @@ App::patch('/v1/videos/:videoId/subtitles/:subtitleId') throw new Exception(Exception::VIDEO_SUBTITLE_NOT_FOUND); } - $subtitle->setAttribute('videoId', $videoId) - ->setAttribute('bucketId', $bucketId) - ->setAttribute('fileId', $fileId) + $subtitle->setAttribute('videoId', $video->getId()) + ->setAttribute('videoInternalId', $video->getInternalId()) + ->setAttribute('bucketId', $file->getAttribute('bucketId')) + ->setAttribute('bucketInternalId', $file->getAttribute('bucketInternalId')) + ->setAttribute('fileId', $file->getId()) + ->setAttribute('fileInternalId', $file->getInternalId()) ->setAttribute('name', $name) ->setAttribute('code', $code) ->setAttribute('default', $default); @@ -1276,9 +1302,9 @@ App::patch('/v1/videos/profiles/:profileId') $profile->setAttribute('name', $name) ->setAttribute('videoBitRate', (int)$videoBitRate) - ->setAttribute('audioBitRate', (int)$audioBitRate) - ->setAttribute('width', (int)$width) - ->setAttribute('height', (int)$height); + ->setAttribute('audioBitRate', (int)$audioBitRate) + ->setAttribute('width', (int)$width) + ->setAttribute('height', (int)$height); $profile = Authorization::skip(fn() => $dbForProject->updateDocument('videos_profiles', $profile->getId(), $profile)); diff --git a/app/init.php b/app/init.php index 50738ea54f..4626564537 100644 --- a/app/init.php +++ b/app/init.php @@ -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 = '644a75323601130be4a8'; + $projectId = '64575eb69a8995fbde21'; if ($projectId === 'console') { return $console; @@ -936,7 +936,6 @@ App::setResource('console', function () { App::setResource('dbForProject', function ($db, $cache, Document $project) { $cache = new Cache(new RedisCache($cache)); - $database = new Database(new MariaDB($db), $cache); $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); $database->setNamespace("_{$project->getInternalId()}"); diff --git a/app/workers/videos.php b/app/workers/videos.php index 901c689716..8be3df0c2d 100644 --- a/app/workers/videos.php +++ b/app/workers/videos.php @@ -46,6 +46,7 @@ class VideosV1 extends Worker private string $basePath = '/tmp/'; //private string $basePath = '/usr/src/code/tests/tmp/'; private string $inDir; + private string $inPath; private string $outDir; private string $outPath; private string $renditionName; @@ -81,20 +82,18 @@ class VideosV1 extends Worker public function run(): void { - $startTime = time(); + $this->database = $this->getProjectDB($this->project->getId()); $this->bucket = $this->database->getDocument('buckets', $this->video->getAttribute('bucketId')); $this->file = $this->database->getDocument('bucket_' . $this->bucket->getInternalId(), $this->video->getAttribute('fileId')); $path = basename($this->file->getAttribute('path')); - $inPath = $this->inDir . $path; + $this->inPath = $this->inDir . $path; /** * Write original asset to tmp */ $result = $this->write($this->project, $this->file); - console::info('Transferring video from storage to ' . $this->inDir); - if (empty($result)) { console::error('Storage transfer error'); } @@ -103,18 +102,17 @@ class VideosV1 extends Worker . 'X' . $this->profile->getAttribute('height') . '@' . ($this->profile->getAttribute('videoBitRate') + $this->profile->getAttribute('audioBitRate')); - /** * FFMpeg init */ $this->ffprobe = FFProbe::create(); $this->ffmpeg = FFMpeg::create([ 'timeout' => 0, - 'ffmpeg.threads' => 12 + 'ffmpeg.threads' => 12 ]); - if (!$this->ffprobe->isValid($inPath)) { - console::error('Not an valid Video file "' . $inPath . '"'); + if (!$this->ffprobe->isValid($this->inPath)) { + console::error('Not an valid Video file "' . $this->inPath . '"'); } if (empty($this->video->getAttribute('duration'))) { @@ -122,7 +120,7 @@ class VideosV1 extends Worker * Original asset metadata */ $mediaInfo = new MediaInfo(); - $mediaInfoContainer = $mediaInfo->getInfo($inPath); + $mediaInfoContainer = $mediaInfo->getInfo($this->inPath); $general = $mediaInfoContainer->getGeneral(); $this->video ->setAttribute('duration', $general->has('duration') ? $general->get('duration')->getMilliseconds() : 0) @@ -166,186 +164,208 @@ class VideosV1 extends Worker ); } $this->video->getAttribute('videoBitRate'); - $media = $this->ffmpeg->open($inPath); + $media = $this->ffmpeg->open($this->inPath); - if ($this->action === 'preview') { - if (!$this->isVideo) { - return; - } - console::info('Creating preview image from second ' . $this->args['second']); + switch ($this->action) { + case 'preview': + $this->createPreview($media); + break; - $path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/preview/'; - $name = 'preview.jpg'; - $media - ->filters() - ->resize(new \FFMpeg\Coordinate\Dimension($this->video->getAttribute('width'), $this->video->getAttribute('height'))) - ->synchronize(); - $media - ->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds($this->args['second'])) - ->save($this->outDir . $name); + case 'timeline': + $this->createTimeLine($media); + break; - /** - * Upload preview - */ - console::info('Uploading ' . $name); + default: + $this->createOutput($media); + break; + } - $this->getVideoDevice($this->project->getId())->write( - $path . $name, - (new Local('/'))->read($this->outDir . $name), - mime_content_type($this->outDir . $name) + unset($media); + } + + + private function createPreview($media): void + { + + if (!$this->isVideo) { + return; + } + console::info('Creating preview image from second ' . $this->args['second']); + + $path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/preview/'; + $name = 'preview.jpg'; + $media + ->filters() + ->resize(new \FFMpeg\Coordinate\Dimension($this->video->getAttribute('width'), $this->video->getAttribute('height'))) + ->synchronize(); + $media + ->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds($this->args['second'])) + ->save($this->outDir . $name); + + /** + * Upload preview + */ + console::info('Uploading ' . $name); + + $this->getVideoDevice($this->project->getId())->write( + $path . $name, + (new Local('/'))->read($this->outDir . $name), + mime_content_type($this->outDir . $name) + ); + + $preview = $this->database->findOne('videos_previews', [ + Query::equal('videoId', [$this->video->getId()]), + Query::equal('type', [$this->action]), + Query::equal('name', [$name]), + ]); + + if (empty($preview)) { + $preview = $this->database->createDocument('videos_previews', new Document([ + 'videoId' => $this->video->getId(), + 'videoInternalId' => $this->video->getInternalId(), + 'type' => $this->action, + 'name' => $name, + 'path' => $path, + 'second' => $this->args['second'], + ])); + + $this->video->setAttribute('previewId', $preview->getId()); + $this->database->updateDocument( + 'videos', + $this->video->getId(), + new document( + array_filter((array)$this->video, fn($value) => !is_null($value)) + ), ); + } else { + $this->database->updateDocument( + 'videos_previews', + $preview->getId(), + $preview->setAttribute('second', $this->args['second']) + ); + /** + * Clean preview cache + */ + (new Delete()) + ->setType(DELETE_TYPE_CACHE_BY_RESOURCE) + ->setResource('preview/' . $preview->getId()) + ->trigger(); + } + } - $preview = $this->database->findOne('videos_previews', [ - Query::equal('videoId', [$this->video->getId()]), - Query::equal('type', [$this->action]), - Query::equal('name', [$name]), - ]); - - if (empty($preview)) { - $preview = $this->database->createDocument('videos_previews', new Document([ - 'videoId' => $this->video->getId(), - 'type' => $this->action, - 'name' => $name, - 'path' => $path, - 'second' => $this->args['second'], - ])); - - $this->video->setAttribute('previewId', $preview->getId()); - $this->database->updateDocument( - 'videos', - $this->video->getId(), - new document( - array_filter((array)$this->video, fn ($value) => !is_null($value)) - ), - ); - } else { - $this->database->updateDocument( - 'videos_previews', - $preview->getId(), - $preview->setAttribute('second', $this->args['second']) - ); - /** - * Clean preview cache - */ - (new Delete()) - ->setType(DELETE_TYPE_CACHE_BY_RESOURCE) - ->setResource('preview/' . $preview->getId()) - ->trigger(); - } + private function createTimeLine($media): void + { + /* + * Title: PlayerJS Thumbnails & WebVTT Creator + * URI: https://playerjs.com/docs/q=thumbnailsphpwebvtt + * Version: 1.0 + * Author: Playerjs.com + * Author URI: https://playerjs.com + * License: GPL-2.0+ + * License URI: http://www.gnu.org/licenses/gpl-2.0.txt + * Text Domain: playerjs + */ + if (!$this->isVideo) { return; } - if ($this->action === 'timeline') { - /* - * Title: PlayerJS Thumbnails & WebVTT Creator - * URI: https://playerjs.com/docs/q=thumbnailsphpwebvtt - * Version: 1.0 - * Author: Playerjs.com - * Author URI: https://playerjs.com - * License: GPL-2.0+ - * License URI: http://www.gnu.org/licenses/gpl-2.0.txt - * Text Domain: playerjs - */ - if (!$this->isVideo) { - return; + $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; } - - $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'; - - $cmd = [ - '/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 . 'sprite%d.jpg', - ]; - $result = shell_exec(implode(" ", $cmd)); - - /** - * Vtt creation* - */ - if ($result !== false) { - $size = explode('x', $timeline['size']); - $counter = 0; - $images = ceil((($this->video->getAttribute('duration') / 1000) / $interval) / ($size[0] * $size[1])); - $data = "WEBVTT"; - $path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/timeline/'; - for ($image = 1; $image <= $images; $image++) { - $sprite = $this->database->createDocument('videos_previews', new Document([ - 'videoId' => $this->video->getId(), - 'type' => 'sprite', - 'name' => 'sprite' . $image . '.jpg', - 'path' => $path, - ])); - - $url = TMP_HOST . 'v1/videos/' . $this->video->getId() . '/preview/' . $sprite->getId() . '/'; - 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" . $url . "#xywh=" . ($row * $timeline['width']) . "," . ($col * $timeline['height']) . "," . $timeline['width'] . "," . $timeline['height']; - $counter++; - } - } - } - - if ($counter > 0) { - /** - * Upload vtt - */ - $this->getVideoDevice($this->project->getId())->write( - $this->getVideoDevice($this->project->getId())->getPath($this->video->getId() . '/timeline/timeline.vtt'), - $data, - 'text/vtt' - ); - - console::info('Uploading timeline vtt'); - - /** - * Upload sprites* - */ - $dir = new DirectoryIterator($this->outDir); - foreach ($dir as $fileinfo) { - if (!$fileinfo->isDot()) { - console::info('Uploading ' . $fileinfo->getFilename()); - $this->getVideoDevice($this->project->getId())->write( - $path . $fileinfo->getFilename(), - (new Local('/'))->read($this->outDir . $fileinfo->getFilename()), - mime_content_type($this->outDir . $fileinfo->getFilename()) - ); - } - } - } - } - - return; } + $timeline['aspect'] = $this->video->getAttribute('width') / $this->video->getAttribute('height'); + $timeline['width'] = 160; + $timeline['height'] = round($timeline['width'] / $timeline['aspect']); + $timeline['size'] = '5x5'; + + $cmd = [ + '/usr/bin/ffmpeg', + '-i ' . $this->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 . 'sprite%d.jpg', + ]; + $result = shell_exec(implode(" ", $cmd)); + + /** + * Vtt creation* + */ + if ($result !== false) { + $size = explode('x', $timeline['size']); + $counter = 0; + $images = ceil((($this->video->getAttribute('duration') / 1000) / $interval) / ($size[0] * $size[1])); + $data = "WEBVTT"; + $path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/timeline/'; + for ($image = 1; $image <= $images; $image++) { + $sprite = $this->database->createDocument('videos_previews', new Document([ + 'videoId' => $this->video->getId(), + 'videoInternalId' => $this->video->getInternalId(), + 'type' => 'sprite', + 'name' => 'sprite' . $image . '.jpg', + 'path' => $path, + ])); + + $url = TMP_HOST . 'v1/videos/' . $this->video->getId() . '/preview/' . $sprite->getId() . '/'; + 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" . $url . "#xywh=" . ($row * $timeline['width']) . "," . ($col * $timeline['height']) . "," . $timeline['width'] . "," . $timeline['height']; + $counter++; + } + } + } + + if ($counter > 0) { + /** + * Upload vtt + */ + $this->getVideoDevice($this->project->getId())->write( + $this->getVideoDevice($this->project->getId())->getPath($this->video->getId() . '/timeline/timeline.vtt'), + $data, + 'text/vtt' + ); + + console::info('Uploading timeline vtt'); + + /** + * Upload sprites* + */ + $dir = new DirectoryIterator($this->outDir); + foreach ($dir as $fileinfo) { + if (!$fileinfo->isDot()) { + console::info('Uploading ' . $fileinfo->getFilename()); + $this->getVideoDevice($this->project->getId())->write( + $path . $fileinfo->getFilename(), + (new Local('/'))->read($this->outDir . $fileinfo->getFilename()), + mime_content_type($this->outDir . $fileinfo->getFilename()) + ); + } + } + } + } + } + + private function createOutput($media): void + { $subs = []; - $subtitles = $this->database->find('videos_subtitles', [ + $subtitles = $this->database->find('videos_subtitles', [ Query::equal('videoId', [$this->video->getId()]), Query::equal('status', ['']) ]); @@ -354,7 +374,7 @@ class VideosV1 extends Worker $subtitle->setAttribute('status', self::STATUS_START); $this->database->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle); $bucket = $this->database->getDocument('buckets', $subtitle->getAttribute('bucketId')); - $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $subtitle->getAttribute('fileId')); + $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $subtitle->getAttribute('fileId')); $path = basename($file->getAttribute('path')); $this->write($this->project, $file); @@ -369,40 +389,41 @@ class VideosV1 extends Worker } $subs[] = [ - 'name' => $subtitle->getAttribute('name'), - 'code' => $subtitle->getAttribute('code'), - 'path' => $subtitlePath, + 'name' => $subtitle->getAttribute('name'), + 'code' => $subtitle->getAttribute('code'), + 'path' => $subtitlePath, ]; } $query = $this->database->createDocument('videos_renditions', new Document([ - 'videoId' => $this->video->getId(), - 'profileId' => $this->profile->getId(), - 'name' => $this->renditionName, - 'startedAt' => DateTime::now(), - 'status' => self::STATUS_START, - 'width' => $this->profile->getAttribute('width'), - 'height' => $this->profile->getAttribute('height'), - 'videoBitRate' => $this->profile->getAttribute('videoBitRate'), - 'audioBitRate' => $this->profile->getAttribute('audioBitRate'), - 'output' => $this->args['output'], - ])); + 'videoId' => $this->video->getId(), + 'videoInternalId' => $this->video->getInternalId(), + 'profileId' => $this->profile->getId(), + 'profileInternalId' => $this->profile->getInternalId(), + 'name' => $this->renditionName, + 'startedAt' => DateTime::now(), + 'status' => self::STATUS_START, + 'width' => $this->profile->getAttribute('width'), + 'height' => $this->profile->getAttribute('height'), + 'videoBitRate' => $this->profile->getAttribute('videoBitRate'), + 'audioBitRate' => $this->profile->getAttribute('audioBitRate'), + 'output' => $this->args['output'], + ])); $this->send($query); $renditionRootPath = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/'; - $renditionPath = $renditionRootPath . $this->renditionName . '-' . $query->getId() . '/'; + $renditionPath = $renditionRootPath . $this->renditionName . '-' . $query->getId() . '/'; try { $representation = (new Representation()) ->setKiloBitRate($this->profile->getAttribute('videoBitRate')) ->setAudioKiloBitRate($this->profile->getAttribute('audioBitRate')) - ->setResize($this->profile->getAttribute('width'), $this->profile->getAttribute('height')) - ; + ->setResize($this->profile->getAttribute('width'), $this->profile->getAttribute('height')); console::info('Output video id:' . $this->video->getId() . PHP_EOL . 'Output name: ' . $this->file->getAttribute('name') . PHP_EOL . - 'Output width: ' . $this->profile->getAttribute('width') . PHP_EOL . + 'Output width: ' . $this->profile->getAttribute('width') . PHP_EOL . 'Output height: ' . $this->profile->getAttribute('height') . PHP_EOL . 'Output video BitRate:' . $this->profile->getAttribute('videoBitRate') . PHP_EOL . 'Output audio BitRate:' . $this->profile->getAttribute('audioBitRate') . PHP_EOL . @@ -419,7 +440,6 @@ class VideosV1 extends Worker $this->transcode($media, $format, $representation, $subs); - unset($media); //exec('/usr/bin/ffmpeg -y -i /usr/src/code/tests/tmp/637f59c88f9ff0fe3b1f/637e1b82aeab8980400e/in/637f59ab5bce0e36d05e.mp4 -c:v libx264 -c:a aac -bf 1 -keyint_min 25 -g 250 -sc_threshold 40 -use_timeline 0 -use_template 0 -seg_duration 10 -hls_playlist 0 -f dash -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)" -map 0 -s:v:0 1024x576 -b:v:0 2538k -b:a:0 128k -strict -2 -threads 12 /usr/src/code/tests/tmp/637f59c88f9ff0fe3b1f/637e1b82aeab8980400e/out/637f59c88f9ff0fe3b1f.mpd2>&1', $o, $v); //var_dump($o); //var_dump($v); @@ -431,12 +451,13 @@ class VideosV1 extends Worker if (!empty($m3u8['segments'])) { foreach ($m3u8['segments'] as $segment) { $this->database->createDocument('videos_renditions_segments', new Document([ - 'renditionId' => $query->getId(), - 'streamId' => (int)$stream['id'], - 'fileName' => $segment['fileName'], - 'path' => $renditionPath, - 'duration' => $segment['duration'], - ])); + 'renditionId' => $query->getId(), + 'renditionInternalId' => $query->getInternalId(), + 'streamId' => (int)$stream['id'], + 'fileName' => $segment['fileName'], + 'path' => $renditionPath, + 'duration' => $segment['duration'], + ])); } } @@ -448,13 +469,14 @@ class VideosV1 extends Worker if (!empty($mpd['segments'])) { foreach ($mpd['segments'] as $segment) { - $this->database->createDocument('videos_renditions_segments', new Document([ - 'renditionId' => $query->getId(), - 'streamId' => $segment['streamId'], - 'fileName' => $segment['fileName'], - 'path' => $renditionPath, - 'isInit' => $segment['isInit'], - ])); + $this->database->createDocument('videos_renditions_segments', new Document([ + 'renditionId' => $query->getId(), + 'renditionInternalId' => $query->getInternalId(), + 'streamId' => $segment['streamId'], + 'fileName' => $segment['fileName'], + 'path' => $renditionPath, + 'isInit' => $segment['isInit'], + ])); } } @@ -472,12 +494,13 @@ class VideosV1 extends Worker if ($this->args['output'] === self::OUTPUT_HLS) { $m3u8 = $this->getSegments($this->outPath . '_subtitles_' . $subtitle['code'] . '.m3u8'); foreach ($m3u8['segments'] ?? [] as $segment) { - $this->database->createDocument('videos_subtitles_segments', new Document([ - 'subtitleId' => $subtitle->getId(), - 'fileName' => $segment['fileName'], - 'path' => $renditionRootPath . 'subtitles/', - 'duration' => $segment['duration'], - ])); + $this->database->createDocument('videos_subtitles_segments', new Document([ + 'subtitleId' => $subtitle->getId(), + 'subtitleInternalId' => $subtitle->getInternalId(), + 'fileName' => $segment['fileName'], + 'path' => $renditionRootPath . 'subtitles/', + 'duration' => $segment['duration'], + ])); } $subtitle->setAttribute('targetDuration', $m3u8['targetDuration']); } else { @@ -491,7 +514,7 @@ class VideosV1 extends Worker console::info('Rendition ' . $query->getId() . ' conversion, done'); - /** Upload**/ + /** Upload **/ $dir = new DirectoryIterator($this->outDir); foreach ($dir as $fileinfo) { if (!$fileinfo->isDot()) { @@ -504,12 +527,12 @@ class VideosV1 extends Worker console::info('Uploading ' . $fileinfo->getFilename()); $this->getVideoDevice($this->project->getId())->write( - $to . $fileinfo->getFilename(), + $to . $fileinfo->getFilename(), $data, mime_content_type($this->outDir . $fileinfo->getFilename()) ); - if ($fileinfo->key() === 0) { + if ($fileinfo->key() === 0) { $query->setAttribute('progress', '100'); $query->setAttribute('status', self::STATUS_UPLOADING); $query->setAttribute('path', $renditionPath); @@ -522,8 +545,6 @@ class VideosV1 extends Worker $query->setAttribute('status', self::STATUS_READY); $this->database->updateDocument('videos_renditions', $query->getId(), $query); $this->send($query, 'update'); - - Console::warning('Job total time ' . (microtime(true) - $startTime) . ' seconds'); } catch (\Throwable $th) { $query->setAttribute('metadata', json_encode([ 'code' => $th->getCode(), @@ -768,6 +789,7 @@ class VideosV1 extends Worker */ private function write(Document $project, Document $file): bool { + console::info('Writing file to ' . $this->inDir); $fullPath = $file->getAttribute('path'); $path = basename($file->getAttribute('path'));