mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
re-working worker flow
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+231
-274
@@ -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
|
||||
|
||||
Generated
+8
-11
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user