From e0f467357490804446b82fe859b638efcd6f270b Mon Sep 17 00:00:00 2001 From: shimon Date: Wed, 17 May 2023 11:37:46 +0300 Subject: [PATCH] re-working worker flow --- app/config/collections.php | 22 -- app/controllers/api/videos.php | 84 ++---- app/init.php | 2 +- app/workers/videos.php | 505 +++++++++++++++------------------ composer.lock | 19 +- src/Appwrite/Event/Video.php | 25 +- 6 files changed, 283 insertions(+), 374 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index e44322d522..b0703a0aea 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -3640,28 +3640,6 @@ $collections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => 'previewId', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - '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, diff --git a/app/controllers/api/videos.php b/app/controllers/api/videos.php index b3b129ccdf..9b2c44910c 100644 --- a/app/controllers/api/videos.php +++ b/app/controllers/api/videos.php @@ -108,12 +108,6 @@ App::post('/v1/videos') ])); }); - (new Video()) - ->setAction('preview') - ->setProject($project) - ->setVideo($video) - ->trigger(); - (new Video()) ->setAction('timeline') ->setProject($project) @@ -328,50 +322,6 @@ App::get('/v1/videos/:videoId/timeline') ->send($data); }); -App::post('/v1/videos/:videoId/preview') - ->desc('Create Video preview from a given point in duration (second)') - ->groups(['api', 'videos']) - ->label('scope', 'videos.write') - ->label('audits.event', 'preview.create') - ->label('audits.resource', 'video/{response.$id}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'videos') - ->label('sdk.method', 'createPreview') - ->label('sdk.description', '/docs/references/videos/create-preview.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.') - ->param('second', 5, new Numeric(), 'Second from video duration.') - ->inject('request') - ->inject('response') - ->inject('project') - ->inject('dbForProject') - ->inject('mode') - ->action(action: function (string $videoId, int $second, Request $request, Response $response, Document $project, 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); - - $range = new Range(1, ($video['duration'] / 1000), Validator::TYPE_INTEGER); - if (!$range->isValid($second)) { - throw new Exception(Exception::VIDEO_SECOND_OUT_OF_RANGE); - } - - (new Video()) - ->setAction('preview') - ->setProject($project) - ->setVideo($video) - ->setSecond($second) - ->trigger(); - - $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic($video, Response::MODEL_VIDEO); - }); App::get('/v1/videos/:videoId/preview/:previewId') ->desc('Get video file preview') @@ -466,9 +416,10 @@ App::post('/v1/videos/:videoId/subtitles') ->param('default', false, new Boolean(true), 'Default subtitle.') ->inject('request') ->inject('response') + ->inject('project') ->inject('dbForProject') ->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) { + ->action(action: function (string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Request $request, Response $response, Document $project, Database $dbForProject, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); @@ -496,6 +447,13 @@ App::post('/v1/videos/:videoId/subtitles') 'default' => $default, ]))); + (new Video()) + ->setAction('subtitle') + ->setProject($project) + ->setVideo($video) + ->setSubtitle($subtitle) + ->trigger(); + $response->setStatusCode(Response::STATUS_CODE_CREATED); $response->dynamic($subtitle, Response::MODEL_SUBTITLE); }); @@ -553,9 +511,10 @@ App::patch('/v1/videos/:videoId/subtitles/:subtitleId') ->param('code', '', new Text(3), 'Subtitle ISO 639-2 three letters alpha code.') ->param('default', false, new Boolean(true), 'Default subtitle.') ->inject('response') + ->inject('project') ->inject('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) { + ->action(action: function (string $subtitleId, string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Response $response, Document $project, Database $dbForProject, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); @@ -597,6 +556,13 @@ App::patch('/v1/videos/:videoId/subtitles/:subtitleId') $subtitle = Authorization::skip(fn() => $dbForProject->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle)); + (new Video()) + ->setAction('subtitle') + ->setProject($project) + ->setVideo($video) + ->setSubtitle($subtitle) + ->trigger(); + $response->dynamic($subtitle, Response::MODEL_SUBTITLE); }); @@ -639,6 +605,14 @@ App::delete('/v1/videos/:videoId/subtitles/:subtitleId') throw new Exception('Failed to remove video subtitle', 500, Exception::GENERAL_SERVER_ERROR); } + $segments = Authorization::skip(fn() => $dbForProject->find('videos_subtitles_segments', [ + Query::equal('subtitleInternalId', [$subtitle->getInternalId()]), + ])); + + foreach ($segments as $segment) { + $dbForProject->deleteDocument('videos_subtitles_segments', $segment->getId()); + } + $response->noContent(); }); @@ -878,8 +852,6 @@ App::get('/v1/videos/:videoId/outputs/:output/:fileName') Query::equal('status', ['ready']), ])); - var_dump('$dbTime = ' . microtime(true) - $dbStartTime); - $_renditions = $_subtitles = []; if ($output === 'hls') { @@ -1157,7 +1129,7 @@ App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId/:fileName') } validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode); - + //var_dump('$subtitleId=',$subtitleId); $subtitle = Authorization::skip(fn () => $dbForProject->findOne('videos_subtitles', [ Query::equal('_uid', [$subtitleId]), Query::equal('status', ['ready']), @@ -1176,7 +1148,7 @@ App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId/:fileName') $segments = Authorization::skip(fn () => $dbForProject->find('videos_subtitles_segments', [ Query::equal('subtitleId', [$subtitleId]), ])); - + //var_dump('$segments=',$segments); if (empty($segments)) { throw new Exception(Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND); } diff --git a/app/init.php b/app/init.php index d26b8d6802..e276fd898d 100644 --- a/app/init.php +++ b/app/init.php @@ -907,7 +907,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) { /** @var Utopia\Database\Document $console */ $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console')); - $projectId = '645905a0594ef29a8680'; + $projectId = '64635cdd5f809d98d3ad'; if ($projectId === 'console') { return $console; diff --git a/app/workers/videos.php b/app/workers/videos.php index b26af740e1..dd80110b8d 100644 --- a/app/workers/videos.php +++ b/app/workers/videos.php @@ -54,6 +54,7 @@ class VideosV1 extends Worker private string $renditionName; private Database $database; private Document $video; + private Document $subtitle; private string $action; private Document $profile; private Document $project; @@ -61,7 +62,7 @@ class VideosV1 extends Worker private Document $bucket; private FFProbe $ffprobe; private FFMpeg $ffmpeg; - private bool $isVideo = false; + public function getName(): string { @@ -70,9 +71,11 @@ class VideosV1 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->video = new Document($this->args['video'] ?? []); + $this->profile = new Document($this->args['profile'] ?? []); + $this->project = new Document($this->args['project'] ?? []); + $this->subtitle = new Document($this->args['subtitle'] ?? []); + $this->action = $this->args['action']; $this->basePath .= uniqid(); $this->inDir = $this->basePath . '/in/'; @@ -92,172 +95,22 @@ class VideosV1 extends Worker $path = basename($this->file->getAttribute('path')); $this->inPath = $this->inDir . $path; - /** - * Write original asset to tmp - */ - $result = $this->write($this->project, $this->file); - - if (empty($result)) { - console::error('Storage transfer error'); - } - - $this->renditionName = $this->profile->getAttribute('width') - . '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' => 4 - ]); - - if (!$this->ffprobe->isValid($this->inPath)) { - console::error('Not an valid Video file "' . $this->inPath . '"'); - } - - if (empty($this->video->getAttribute('duration'))) { - /** - * Original asset metadata - */ - $mediaInfo = new MediaInfo(); - $mediaInfoContainer = $mediaInfo->getInfo($this->inPath); - $general = $mediaInfoContainer->getGeneral(); - $this->video - ->setAttribute('duration', $general->has('duration') ? $general->get('duration')->getMilliseconds() : 0) - ->setAttribute('format', $general->has('format') ? $general->get('format')->getShortName() : ''); - - foreach ($mediaInfoContainer->getVideos() ?? [] as $video) { - $this->isVideo = true; - $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') ? $video->get('bit_rate')->getAbsoluteValue() : 0); - } - - 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') ? $audio->get('bit_rate')->getAbsoluteValue() : 0); - } - - 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(), - new document( - array_filter((array)$this->video, fn($value) => !is_null($value)) - ), - ); - } - $this->video->getAttribute('videoBitRate'); - $media = $this->ffmpeg->open($this->inPath); match ($this->action) { - 'preview' => $this->createPreview($media), - 'timeline' => $this->createTimeLine($media), - default => $this->createOutput($media), + 'timeline' => $this->createTimeLine(), + 'subtitle' => $this->createSubtitle(), + default => $this->createOutput(), }; - - unset($media); } /** - * @throws Authorization - * @throws Throwable - * @throws Structure - */ - 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('videoInternalId', [$this->video->getInternalId()]), - 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->video->setAttribute('previewInternalId', $preview->getInternalId()); - $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(); - } - } - - /** + * Create timeline + * * @throws Authorization * @throws Structure * @throws Throwable */ - private function createTimeLine($media): void + private function createTimeLine(): void { /* @@ -270,7 +123,14 @@ class VideosV1 extends Worker * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * Text Domain: playerjs */ - if (!$this->isVideo) { + + $this->download($this->project, $this->file); + + if (empty($this->video->getAttribute('duration'))) { + $this->createInfo(); + } + + if (empty($this->video->getAttribute('videoBitRate'))) { return; } @@ -349,7 +209,7 @@ class VideosV1 extends Worker console::info('Uploading timeline vtt'); /** - * Upload sprites* + * Upload sprites */ $dir = new DirectoryIterator($this->outDir); foreach ($dir as $fileinfo) { @@ -367,44 +227,106 @@ class VideosV1 extends Worker } /** + * Create subtitle + * + * @throws Authorization + * @throws Structure + * @throws Throwable + */ + private function createSubtitle(): void + { + + if (empty($this->video->getAttribute('duration'))) { + $this->download($this->project, $this->file); + $this->createInfo(); + } + + $segments = $this->database->find('videos_subtitles_segments', [ + Query::equal('subtitleInternalId', [$this->subtitle->getInternalId()]), + ]); + + foreach ($segments as $segment) { + $this->database->deleteDocument('videos_subtitles_segments', $segment->getId()); + } + + $this->subtitle->setAttribute('status', self::STATUS_START); + $this->database->updateDocument('videos_subtitles', $this->subtitle->getId(), $this->subtitle); + + $bucket = $this->database->getDocument('buckets', $this->subtitle->getAttribute('bucketId')); + $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $this->subtitle->getAttribute('fileId')); + $path = basename($file->getAttribute('path')); + + $this->download($this->project, $file); + + $ext = pathinfo($path, PATHINFO_EXTENSION); + $subtitlePath = $this->inDir . $this->subtitle->getId() . '.vtt'; + + if ($ext === 'srt') { + $srt = new SubripFile($this->inDir . $path); + $srt->convertTo('webvtt')->save($subtitlePath); + } + + $path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/subtitles/'; + $this->database->createDocument('videos_subtitles_segments', new Document([ + 'subtitleId' => $this->subtitle->getId(), + 'subtitleInternalId' => $this->subtitle->getInternalId(), + 'fileName' => $this->subtitle->getId() . '.vtt', + 'path' => $path, + 'duration' => (string)number_format(($this->video->getAttribute('duration') / 1000), 1), + ])); + + /** + * Upload subtitle + */ + console::info('Uploading ' . $this->subtitle->getId() . '.vtt'); + $this->getVideoDevice($this->project->getId())->write( + $path . $this->subtitle->getId() . '.vtt', + (new Local('/'))->read($this->inDir . $this->subtitle->getId() . '.vtt'), + 'text/vtt' + ); + + $this->subtitle->setAttribute('targetDuration', (string)number_format(($this->video->getAttribute('duration') / 1000), 1)); + $this->subtitle->setAttribute('status', self::STATUS_READY); + $this->subtitle->setAttribute('path', $path); + $this->database->updateDocument('videos_subtitles', $this->subtitle->getId(), $this->subtitle); + } + + /** + * Create output + * * @throws Authorization * @throws Exception * @throws Throwable * @throws Structure */ - private function createOutput($media): void + private function createOutput(): void { - $subs = []; - $subtitles = $this->database->find('videos_subtitles', [ - Query::equal('videoInternalId', [$this->video->getInternalId()]), - Query::equal('status', ['']) + + $this->download($this->project, $this->file); + if (empty($this->video->getAttribute('duration'))) { + $this->createInfo(); + } + + /** + * FFMpeg init + */ + $this->ffprobe = FFProbe::create(); + $this->ffmpeg = FFMpeg::create([ + 'timeout' => 0, + 'ffmpeg.threads' => 4 ]); - foreach ($subtitles as $subtitle) { - $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')); - $path = basename($file->getAttribute('path')); - $this->write($this->project, $file); - - console::info('Transferring subtitle from storage to ' . $this->inDir); - - $ext = pathinfo($path, PATHINFO_EXTENSION); - $subtitlePath = $this->inDir . $subtitle->getId() . '.vtt'; - - if ($ext === 'srt') { - $srt = new SubripFile($this->inDir . $path); - $srt->convertTo('webvtt')->save($subtitlePath); - } - - $subs[] = [ - 'name' => $subtitle->getAttribute('name'), - 'code' => $subtitle->getAttribute('code'), - 'path' => $subtitlePath, - ]; + if (!$this->ffprobe->isValid($this->inPath)) { + console::error('Not an valid Video file "' . $this->inPath . '"'); } + $media = $this->ffmpeg->open($this->inPath); + + $this->renditionName = $this->profile->getAttribute('width') + . 'X' . $this->profile->getAttribute('height') + . '@' . ($this->profile->getAttribute('videoBitRate') + $this->profile->getAttribute('audioBitRate')); + + $query = $this->database->createDocument('videos_renditions', new Document([ 'videoId' => $this->video->getId(), 'videoInternalId' => $this->video->getInternalId(), @@ -422,9 +344,6 @@ class VideosV1 extends Worker $this->send($query); - $renditionRootPath = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/'; - $renditionPath = $renditionRootPath . $this->renditionName . '-' . $query->getId() . '/'; - try { $representation = (new Representation()) ->setKiloBitRate($this->profile->getAttribute('videoBitRate')) @@ -448,12 +367,16 @@ class VideosV1 extends Worker } }); - $this->transcode($media, $format, $representation, $subs); + $this->transcode($media, $format, $representation); + + 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); + $path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/' . $this->renditionName . '-' . $query->getId() . '/'; + if ($this->args['output'] === self::OUTPUT_HLS) { $streams = $this->getHlsSegmentsUrls($this->outDir . 'master.m3u8'); foreach ($streams as $stream) { @@ -465,7 +388,7 @@ class VideosV1 extends Worker 'renditionInternalId' => $query->getInternalId(), 'streamId' => (int)$stream['id'], 'fileName' => $segment['fileName'], - 'path' => $renditionPath, + 'path' => $path, 'duration' => $segment['duration'], ])); } @@ -484,7 +407,7 @@ class VideosV1 extends Worker 'renditionInternalId' => $query->getInternalId(), 'streamId' => $segment['streamId'], 'fileName' => $segment['fileName'], - 'path' => $renditionPath, + 'path' => $path, 'isInit' => $segment['isInit'], ])); } @@ -500,44 +423,20 @@ class VideosV1 extends Worker $this->database->updateDocument('videos_renditions', $query->getId(), $query); $this->send($query, 'update'); - foreach ($subtitles ?? [] as $subtitle) { - 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(), - 'subtitleInternalId' => $subtitle->getInternalId(), - 'fileName' => $segment['fileName'], - 'path' => $renditionRootPath . 'subtitles/', - 'duration' => $segment['duration'], - ])); - } - $subtitle->setAttribute('targetDuration', $m3u8['targetDuration']); - } else { - $this->getFilesDevice($this->project->getId())->transfer($this->inDir . $subtitle->getId() . '.vtt', $this->outDir . $subtitle->getId() . '.vtt', $this->getFilesDevice($this->project->getId())); - } - - $subtitle->setAttribute('status', self::STATUS_READY); - $subtitle->setAttribute('path', $renditionRootPath . 'subtitles/'); - $this->database->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle); - } - console::info('Rendition ' . $query->getId() . ' conversion, done'); - /** Upload **/ + /** + * Upload + */ $dir = new DirectoryIterator($this->outDir); foreach ($dir as $fileinfo) { if (!$fileinfo->isDot()) { $data = (new Local('/'))->read($this->outDir . $fileinfo->getFilename()); - $to = $renditionPath; - if (str_contains($fileinfo->getFilename(), "_subtitles_") || str_contains($fileinfo->getFilename(), ".vtt")) { - $to = $renditionRootPath . 'subtitles/'; - } console::info('Uploading ' . $fileinfo->getFilename()); $this->getVideoDevice($this->project->getId())->write( - $to . $fileinfo->getFilename(), + $path . $fileinfo->getFilename(), $data, mime_content_type($this->outDir . $fileinfo->getFilename()) ); @@ -545,7 +444,7 @@ class VideosV1 extends Worker if ($fileinfo->key() === 0) { $query->setAttribute('progress', '100'); $query->setAttribute('status', self::STATUS_UPLOADING); - $query->setAttribute('path', $renditionPath); + $query->setAttribute('path', $path); $this->database->updateDocument('videos_renditions', $query->getId(), $query); $this->send($query, 'update'); } @@ -572,14 +471,70 @@ class VideosV1 extends Worker } } + /** + * Create media Info + * + * @throws Authorization + * @throws Structure + * @throws Throwable + */ + private function createInfo(): void + { + + $mediaInfo = new MediaInfo(); + $mediaInfoContainer = $mediaInfo->getInfo($this->inPath); + $general = $mediaInfoContainer->getGeneral(); + $this->video + ->setAttribute('duration', $general->has('duration') ? $general->get('duration')->getMilliseconds() : 0) + ->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') ? $video->get('bit_rate')->getAbsoluteValue() : 0); + } + + 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') ? $audio->get('bit_rate')->getAbsoluteValue() : 0); + } + + 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(), + new document( + array_filter((array)$this->video, fn($value) => !is_null($value)) + ), + ); + } + + /** + * Transcode + * * @param $media Media * @param $format StreamFormat * @param $representation Representation - * @param array $subtitles * @return void */ - private function transcode(Media $media, StreamFormat $format, Representation $representation, array $subtitles): void + private function transcode(Media $media, StreamFormat $format, Representation $representation): void { $additionalParams = [ @@ -602,14 +557,8 @@ class VideosV1 extends Worker return; } - $hls = $media->hls(); - foreach ($subtitles as $subtitle) { - $sub = new HLSSubtitle($subtitle['path'], $subtitle['name'], $subtitle['code']); - $sub->default(); - $hls->subtitle($sub); - } - - $hls->setFormat($format) + $media->hls() + ->setFormat($format) ->setHlsTime($segmentSize) ->addRepresentation($representation) ->setAdditionalParams($additionalParams) @@ -617,6 +566,8 @@ class VideosV1 extends Worker } /** + * Get HLS|DASH segments + * * @param string $path * @return array */ @@ -685,6 +636,8 @@ class VideosV1 extends Worker /** + * Get HLS segments urls + * * @param string $path * @return array */ @@ -757,11 +710,11 @@ class VideosV1 extends Worker } /** - * Send realtime updates + * Send * * @param string $action * @param Document $payload - * @throws Exception + * @throws Exception|\Exception */ private function send(Document $payload, string $action = 'create') { @@ -790,57 +743,61 @@ class VideosV1 extends Worker ); } + /** - * Write files + * Download * * @param $project Document * @param $file Document - * @return boolean * @throws \Exception */ - private function write(Document $project, Document $file): bool + private function download(Document $project, Document $file) { - console::info('Writing file to ' . $this->inDir); $fullPath = $file->getAttribute('path'); $path = basename($file->getAttribute('path')); + + console::info('Downloading file: ' . $path . ' to local storage ' . $this->inDir); + $local = new Local('/'); - if ( - !empty($file->getAttribute('openSSLCipher')) || - $file->getAttribute('algorithm', 'none') !== 'none' - ) { - $data = $this->getFilesDevice($project->getId())->read($fullPath); - if (!empty($file->getAttribute('openSSLCipher'))) { - $data = OpenSSL::decrypt( - $data, - $file->getAttribute('openSSLCipher'), - App::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')), - 0, - \hex2bin($file->getAttribute('openSSLIV')), - \hex2bin($file->getAttribute('openSSLTag')) - ); - } + try { + if ( + !empty($file->getAttribute('openSSLCipher')) || + $file->getAttribute('algorithm', 'none') !== 'none' + ) { + $data = $this->getFilesDevice($project->getId())->read($fullPath); + if (!empty($file->getAttribute('openSSLCipher'))) { + $data = OpenSSL::decrypt( + $data, + $file->getAttribute('openSSLCipher'), + App::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')), + 0, + \hex2bin($file->getAttribute('openSSLIV')), + \hex2bin($file->getAttribute('openSSLTag')) + ); + } - $algorithm = $file->getAttribute('algorithm', 'none'); - switch ($algorithm) { - case 'zstd': - $compressor = new Zstd(); - $data = $compressor->decompress($data); - break; - case 'gzip': - $compressor = new GZIP(); - $data = $compressor->decompress($data); - break; + $algorithm = $file->getAttribute('algorithm', 'none'); + switch ($algorithm) { + case 'zstd': + $compressor = new Zstd(); + $data = $compressor->decompress($data); + break; + case 'gzip': + $compressor = new GZIP(); + $data = $compressor->decompress($data); + break; + } + $local-> + write($this->inDir . $path, $data, $file->getAttribute('mimeType')); + } else { + $this->getFilesDevice( + $project->getId() + )->transfer($fullPath, $this->inDir . $path, $local); } - $result = $local - ->write($this->inDir . $path, $data, $file->getAttribute('mimeType')); - } else { - $result = $this->getFilesDevice( - $project->getId() - )->transfer($fullPath, $this->inDir . $path, $local); + } catch (\Throwable $th) { + console::error($th->getMessage() . ' on file: ' . $th->getFile() . ' line: ' . $th->getLine); } - - return $result; } private function cleanup(): bool diff --git a/composer.lock b/composer.lock index 2d0b1255ed..ba2b9f75d5 100644 --- a/composer.lock +++ b/composer.lock @@ -135,7 +135,7 @@ "source": { "type": "git", "url": "https://github.com/shimonewman/PHP-FFmpeg-video-streaming.git", - "reference": "0d6013d6f69483fa03f4faef0b53998eef74854d" + "reference": "12d0871597d495d8741d457014144146a0c11018" }, "require": { "php": "^7.2 || ^8.0", @@ -209,7 +209,7 @@ "video-streaming", "video-streaming" ], - "time": "2023-05-01T06:57:01+00:00" + "time": "2023-05-07T11:00:25+00:00" }, { "name": "appwrite/php-clamav", @@ -731,16 +731,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.5.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b964ca597e86b752cd994f27293e9fa6b6a95ed9" + "reference": "733dd89533dd371a0987172727df15f500dab0ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b964ca597e86b752cd994f27293e9fa6b6a95ed9", - "reference": "b964ca597e86b752cd994f27293e9fa6b6a95ed9", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/733dd89533dd371a0987172727df15f500dab0ef", + "reference": "733dd89533dd371a0987172727df15f500dab0ef", "shasum": "" }, "require": { @@ -771,9 +771,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "7.5-dev" } }, "autoload": { @@ -839,7 +836,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.5.1" + "source": "https://github.com/guzzle/guzzle/tree/7.6.0" }, "funding": [ { @@ -855,7 +852,7 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:30:08+00:00" + "time": "2023-05-14T11:23:39+00:00" }, { "name": "guzzlehttp/promises", diff --git a/src/Appwrite/Event/Video.php b/src/Appwrite/Event/Video.php index 2176243459..f544b7bf74 100644 --- a/src/Appwrite/Event/Video.php +++ b/src/Appwrite/Event/Video.php @@ -9,7 +9,7 @@ class Video extends Event { protected ?Document $video = null; protected ?Document $profile = null; - protected int $second = 5; + protected ?Document $subtitle = null; protected string $output = ''; protected string $action = ''; @@ -111,26 +111,26 @@ class Video extends Event } /** - * Sets second from duration. + * Sets subtitle. * - * @param int $second + * @param Document $subtitle * @return self */ - public function setSecond(int $second): self + public function setSubtitle(Document $subtitle): self { - $this->second = $second; + $this->subtitle = $subtitle; return $this; } /** - * Returns second from duration. + * Returns Subtitle. * - * @return int + * @return null|Document */ - public function getSecond(): int + public function getSubtitle(): ?Document { - return $this->second; + return $this->subtitle; } @@ -147,9 +147,14 @@ class Video extends Event 'project' => $this->project, 'user' => $this->user, 'video' => $this->video, - 'second' => $this->getSecond(), ]; + if (!empty($this->getSubtitle())) { + $keys = array_merge($keys, [ + 'subtitle' => $this->getSubtitle(), + ]); + } + if (!empty($this->getOutput())) { $keys = array_merge($keys, [ 'output' => $this->getOutput(),