mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
hls stream
This commit is contained in:
@@ -58,7 +58,7 @@ _APP_SMTP_USERNAME=
|
||||
_APP_SMTP_PASSWORD=
|
||||
_APP_PHONE_PROVIDER=phone://mock
|
||||
_APP_PHONE_FROM=+123456789
|
||||
_APP_STORAGE_LIMIT=300000000000
|
||||
_APP_STORAGE_LIMIT=3000000000000
|
||||
_APP_STORAGE_PREVIEW_LIMIT=20000000
|
||||
_APP_FUNCTIONS_SIZE_LIMIT=30000000
|
||||
_APP_FUNCTIONS_TIMEOUT=900
|
||||
|
||||
+152
-9
@@ -3193,6 +3193,17 @@ $collections = [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'targetDuration',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'duration',
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -3296,7 +3307,7 @@ $collections = [
|
||||
'$id' => 'metadata',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 4096,
|
||||
'size' => 2048,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
@@ -3339,9 +3350,69 @@ $collections = [
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_video_stream',
|
||||
'$id' => '_key_video_stream_profile',
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['videoId', 'stream', 'profileId'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos_renditions_segments' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos_rendition_segments',
|
||||
'$name' => 'Videos_rendition_segments',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'renditionId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'fileName',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'path',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'duration',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_renditionId',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['videoId', 'stream'],
|
||||
'attributes' => ['renditionId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
@@ -3421,11 +3492,11 @@ $collections = [
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_stream',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['stream'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
'$id' => '_key_unique',
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['stream', 'width', 'height', 'videoBitrate', 'audioBitrate' ],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
]
|
||||
],
|
||||
@@ -3510,7 +3581,19 @@ $collections = [
|
||||
'default' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],[
|
||||
],
|
||||
[
|
||||
'$id' => 'targetDuration',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'status',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
@@ -3534,6 +3617,66 @@ $collections = [
|
||||
],
|
||||
]
|
||||
],
|
||||
'videos_subtitles_segments' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'videos_subtitles_segments',
|
||||
'$name' => 'Videos_subtitles_segments',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'subtitleId',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'fileName',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'path',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'duration',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_subtitleId',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['subtitleId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
return $collections;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ return [
|
||||
'audioBitrate' => 64, //audio bitrate in Kbps
|
||||
'width' => 640, //width resolution in px
|
||||
'height' => 360, //height resolution in px
|
||||
'stream' => 'hls'
|
||||
'stream' => 'mpeg-dash'
|
||||
],
|
||||
[
|
||||
'name' => '576p',
|
||||
@@ -16,7 +16,7 @@ return [
|
||||
'audioBitrate' => 128,
|
||||
'width' => 1024,
|
||||
'height' => 576,
|
||||
'stream' => 'hls'
|
||||
'stream' => 'mpeg-dash'
|
||||
],
|
||||
[
|
||||
'name' => '720p',
|
||||
|
||||
@@ -534,6 +534,11 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
$algorithm = empty($compressor) ? '' : $compressor->getName();
|
||||
$fileHash = $deviceFiles->getFileHash($path);
|
||||
|
||||
$openSSLVersion = null;
|
||||
$openSSLCipher = null;
|
||||
$openSSLTag = null;
|
||||
$openSSLIV = null;
|
||||
|
||||
if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) {
|
||||
$openSSLVersion = '1';
|
||||
$openSSLCipher = OpenSSL::CIPHER_AES_128_GCM;
|
||||
|
||||
+529
-141
@@ -1,45 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Validator\Password;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Audit as EventAudit;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Transcoding;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\Stats\Stats;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\View;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Exception\Structure as StructureException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Storage\Compression\Algorithms\GZIP;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Storage\Validator\File;
|
||||
use Utopia\Storage\Validator\FileExt;
|
||||
use Utopia\Storage\Validator\FileSize;
|
||||
use Utopia\Storage\Validator\Upload;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\HexColor;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -96,13 +73,14 @@ function validateFilePermissions(Database $dbForProject, string $bucketId, strin
|
||||
return $file;
|
||||
}
|
||||
|
||||
|
||||
App::post('/v1/videos/profiles')
|
||||
->desc('Create video profile')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'create')
|
||||
->label('sdk.method', 'createProfile')
|
||||
->label('sdk.description', '/docs/references/videos/create-profile.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
@@ -116,7 +94,6 @@ App::post('/v1/videos/profiles')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(action: function (string $name, string $videoBitrate, string $audioBitrate, string $width, string $height, string $stream, Response $response, Database $dbForProject) {
|
||||
|
||||
try {
|
||||
$profile = Authorization::skip(function () use ($dbForProject, $name, $videoBitrate, $audioBitrate, $width, $height, $stream) {
|
||||
return $dbForProject->createDocument('videos_profiles', new Document([
|
||||
@@ -128,21 +105,22 @@ App::post('/v1/videos/profiles')
|
||||
'stream' => $stream,
|
||||
]));
|
||||
});
|
||||
} catch (StructureException $exception) {
|
||||
throw new Exception($exception->getMessage(), 400, Exception::DOCUMENT_INVALID_STRUCTURE);
|
||||
} catch (DuplicateException $exception) {
|
||||
throw new Exception('Profile already exists', 409, Exception::VIDEO_PROFILE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($profile, Response::MODEL_VIDEO_PROFILE);
|
||||
});
|
||||
|
||||
|
||||
App::patch('/v1/videos/profiles/:profileId')
|
||||
->desc('Update video profile')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'update')
|
||||
->label('sdk.method', 'updateProfile')
|
||||
->label('sdk.description', '/docs/references/videos/update-profile.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
@@ -176,6 +154,7 @@ App::patch('/v1/videos/profiles/:profileId')
|
||||
$response->dynamic($profile, Response::MODEL_VIDEO_PROFILE);
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/profiles/:profileId')
|
||||
->desc('Get video profile')
|
||||
->groups(['api', 'video'])
|
||||
@@ -201,6 +180,7 @@ App::get('/v1/videos/profiles/:profileId')
|
||||
$response->dynamic($profile, Response::MODEL_VIDEO_PROFILE);
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/profiles')
|
||||
->desc('Get all video profiles')
|
||||
->groups(['api', 'video'])
|
||||
@@ -228,8 +208,9 @@ App::get('/v1/videos/profiles')
|
||||
]), Response::MODEL_VIDEO_PROFILE_LIST);
|
||||
});
|
||||
|
||||
|
||||
App::delete('/v1/videos/profiles/:profileId')
|
||||
->desc('Delete a video transcoding profile')
|
||||
->desc('Delete video profile')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.write')
|
||||
->label('sdk.namespace', 'video')
|
||||
@@ -280,13 +261,8 @@ App::post('/v1/videos/:videoId/subtitles')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('audits')
|
||||
->inject('usage')
|
||||
->inject('events')
|
||||
->inject('mode')
|
||||
->inject('deviceFiles')
|
||||
->inject('deviceLocal')
|
||||
->action(action: function (string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Request $request, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) {
|
||||
->action(action: function (string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Request $request, Response $response, Database $dbForProject, Document $user, string $mode) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])]));
|
||||
|
||||
@@ -297,8 +273,7 @@ App::post('/v1/videos/:videoId/subtitles')
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
validateFilePermissions($dbForProject, $bucketId, $fileId, $mode, $user);
|
||||
|
||||
try {
|
||||
$subtitle = Authorization::skip(function () use ($dbForProject, $videoId, $bucketId, $fileId, $name, $code, $default) {
|
||||
$subtitle = Authorization::skip(function () use ($dbForProject, $videoId, $bucketId, $fileId, $name, $code, $default) {
|
||||
return $dbForProject->createDocument('videos_subtitles', new Document([
|
||||
'videoId' => $videoId,
|
||||
'bucketId' => $bucketId,
|
||||
@@ -307,15 +282,13 @@ App::post('/v1/videos/:videoId/subtitles')
|
||||
'code' => $code,
|
||||
'default' => $default,
|
||||
]));
|
||||
});
|
||||
} catch (StructureException $exception) {
|
||||
throw new Exception($exception->getMessage(), 400, Exception::DOCUMENT_INVALID_STRUCTURE);
|
||||
}
|
||||
});
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($subtitle, Response::MODEL_VIDEO_SUBTITLE);
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/subtitles')
|
||||
->desc('Get all video subtitles')
|
||||
->groups(['api', 'video'])
|
||||
@@ -344,13 +317,14 @@ App::get('/v1/videos/:videoId/subtitles')
|
||||
]), Response::MODEL_VIDEO_SUBTITLE_LIST);
|
||||
});
|
||||
|
||||
|
||||
App::patch('/v1/videos/:videoId/subtitles/:subtitleId')
|
||||
->desc('Update video subtitle')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'update')
|
||||
->label('sdk.method', 'updateSubtitle')
|
||||
->label('sdk.description', '/docs/references/videos/update-subtitle.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
@@ -384,6 +358,78 @@ App::patch('/v1/videos/:videoId/subtitles/:subtitleId')
|
||||
$response->dynamic($subtitle, Response::MODEL_VIDEO_SUBTITLE);
|
||||
});
|
||||
|
||||
|
||||
App::delete('/v1/videos/:videoId/subtitles/:subtitleId')
|
||||
->desc('Delete video subtitle')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.write')
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'deleteSubtitle')
|
||||
->label('sdk.description', '/docs/references/videos/delete-subtitle.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('videoId', '', new UID(), 'Video unique ID.')
|
||||
->param('subtitleId', '', new UID(), 'Subtitle unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('mode')
|
||||
->action(function (string $videoId, string $subtitleId, Response $response, Database $dbForProject, Document $user, string $mode) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])]));
|
||||
|
||||
if ($video->isEmpty()) {
|
||||
throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
|
||||
|
||||
$subtitle = Authorization::skip(fn() => $dbForProject->getDocument('videos_subtitles', $subtitleId));
|
||||
|
||||
if ($subtitle->isEmpty()) {
|
||||
throw new Exception('Video subtitle not found', 404, Exception::VIDEO_PROFILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deleted = $dbForProject->deleteDocument('videos_subtitles', $subtitleId);
|
||||
|
||||
if (!$deleted) {
|
||||
throw new Exception('Failed to remove video subtitle from DB', 500, Exception::GENERAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::get('/v1/videos/buckets/:bucketId')
|
||||
->desc('Get all backet\'s videos')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'getVideos')
|
||||
->label('sdk.description', '/docs/references/videos/get-videos.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_SUBTITLE_LIST)
|
||||
->param('bucketId', null, new UID(), 'bucket unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function ($videoId, Response $response, Database $dbForProject) {
|
||||
|
||||
$subtitles = Authorization::skip(fn () => $dbForProject->find('videos_subtitles', [new Query('videoId', Query::TYPE_EQUAL, [$videoId])], 12, 0, [], ['ASC']));
|
||||
|
||||
if (empty($subtitles)) {
|
||||
throw new Exception('Video subtitles not found', 404, Exception::VIDEO_SUBTITLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $dbForProject->count('videos_subtitles', [], APP_LIMIT_COUNT),
|
||||
'subtitles' => $subtitles,
|
||||
]), Response::MODEL_VIDEO_SUBTITLE_LIST);
|
||||
});
|
||||
|
||||
|
||||
App::post('/v1/video')
|
||||
->desc('Create Video')
|
||||
->groups(['api', 'video'])
|
||||
@@ -401,14 +447,8 @@ App::post('/v1/video')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('audits')
|
||||
->inject('usage')
|
||||
->inject('events')
|
||||
->inject('mode')
|
||||
->inject('deviceFiles')
|
||||
->inject('deviceLocal')
|
||||
->action(action: function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) {
|
||||
/** @var Utopia\Database\Document $project */
|
||||
->action(action: function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, Document $user, string $mode) {
|
||||
|
||||
$file = validateFilePermissions($dbForProject, $bucketId, $fileId, $mode, $user);
|
||||
$video = Authorization::skip(function () use ($dbForProject, $bucketId, $file) {
|
||||
@@ -420,27 +460,80 @@ App::post('/v1/video')
|
||||
});
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic(new Document([
|
||||
'$id' => $video->getId(),
|
||||
'fileId' => $video['fileId'],
|
||||
'bucketId' => $video['bucketId'],
|
||||
'size' => $video['size'],
|
||||
]), Response::MODEL_VIDEO);
|
||||
$response->dynamic($video, Response::MODEL_VIDEO);
|
||||
});
|
||||
|
||||
App::delete('/v1/videos/:videoId')
|
||||
->desc('Delete video')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.write')
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'delete')
|
||||
->label('sdk.description', '/docs/references/videos/delete.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('videoId', '', new UID(), 'Video unique ID.')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->inject('deviceVideos')
|
||||
->action(function (string $videoId, string $renditionId, Response $response, Document $project, Database $dbForProject, string $mode, Document $user, Device $deviceVideos) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])]));
|
||||
|
||||
if ($video->isEmpty()) {
|
||||
throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
|
||||
$deleted = $dbForProject->deleteDocument('videos', $videoId);
|
||||
|
||||
if (!$deleted) {
|
||||
throw new Exception('Failed to remove video from DB', 500, Exception::GENERAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$renditions = Authorization::skip(fn() => $dbForProject->find('videos_renditions', [
|
||||
new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]),
|
||||
], 12, 0, [], ['ASC']));
|
||||
|
||||
foreach ($renditions as $rendition) {
|
||||
Authorization::skip(fn() => $dbForProject->deleteDocument('videos_renditions', $rendition->getId()));
|
||||
}
|
||||
|
||||
$subtitles = Authorization::skip(fn() => $dbForProject->find('videos_subtitles', [
|
||||
new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]),
|
||||
], 12, 0, [], ['ASC']));
|
||||
|
||||
foreach ($subtitles as $subtitle) {
|
||||
Authorization::skip(fn() => $dbForProject->deleteDocument('videos_subtitles', $subtitle->getId()));
|
||||
}
|
||||
|
||||
foreach ($renditions as $rendition) {
|
||||
Authorization::skip(fn() => $dbForProject->deleteDocument('videos_renditions', $rendition->getId()));
|
||||
}
|
||||
|
||||
$videoPath = $this->getVideoDevice($project->getId())->getPath($this->args['videoId']);
|
||||
$deviceVideos->deletePath($videoPath);
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
|
||||
App::post('/v1/videos/:videoId/rendition')
|
||||
->alias('/v1/videos/:videoId/rendition', [])
|
||||
->desc('Start transcoding video rendition')
|
||||
->desc('Create video rendition')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'createTranscoding')
|
||||
->label('sdk.description', '/docs/references/videos/create-transcoding.md') // TODO: Create markdown
|
||||
->label('sdk.method', 'createRendition')
|
||||
->label('sdk.description', '/docs/references/videos/create-rendition.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
// ->label('event', 'buckets.[bucketId].files.[fileId].create')
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('profileId', '', new CustomId(), 'Profile unique ID.')
|
||||
->inject('request')
|
||||
@@ -448,13 +541,8 @@ App::post('/v1/videos/:videoId/rendition')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('audits')
|
||||
->inject('usage')
|
||||
->inject('events')
|
||||
->inject('mode')
|
||||
->inject('deviceFiles')
|
||||
->inject('deviceLocal')
|
||||
->action(action: function (string $videoId, string $profileId, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) {
|
||||
->action(action: function (string $videoId, string $profileId, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, string $mode) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])]));
|
||||
|
||||
@@ -482,8 +570,45 @@ App::post('/v1/videos/:videoId/rendition')
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/:stream/renditions')
|
||||
->alias('/v1/videos/:videoId/renditions', [])
|
||||
App::get('/v1/videos/:videoId/rendition/:renditionId')
|
||||
->desc('Get all backet\'s videos')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'getRendition')
|
||||
->label('sdk.description', '/docs/references/videos/get-rendition.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_RENDITION)
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('renditionId', null, new UID(), 'Rendition unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->action(function ($videoId, $renditionId, Response $response, Database $dbForProject, string $mode, Document $user) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])]));
|
||||
|
||||
if ($video->isEmpty()) {
|
||||
throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
|
||||
|
||||
$rendition = Authorization::skip(fn () => $dbForProject->findOne('videos_renditions', [new Query('_uid', Query::TYPE_EQUAL, [$renditionId])]));
|
||||
|
||||
if ($rendition->isEmpty()) {
|
||||
throw new Exception('Video rendition not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($rendition, Response::MODEL_VIDEO_RENDITION);
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/renditions')
|
||||
->desc('Get video renditions')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.read')
|
||||
@@ -495,13 +620,11 @@ App::get('/v1/videos/:videoId/:stream/renditions')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VIDEO_RENDITION_LIST)
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('stream', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('usage')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->action(function (string $videoId, string $stream, Response $response, Database $dbForProject, Stats $usage, string $mode, Document $user) {
|
||||
->action(function (string $videoId, Response $response, Database $dbForProject, string $mode, Document $user) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])]));
|
||||
|
||||
@@ -515,10 +638,9 @@ App::get('/v1/videos/:videoId/:stream/renditions')
|
||||
new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]),
|
||||
new Query('endedAt', Query::TYPE_GREATER, [0]),
|
||||
new Query('status', Query::TYPE_EQUAL, ['ready']),
|
||||
new Query('stream', Query::TYPE_EQUAL, [$stream]),
|
||||
];
|
||||
|
||||
$renditions = Authorization::skip(fn () => $dbForProject->find('videos_renditions', $queries, 12, 0, [], ['ASC']));
|
||||
$renditions = Authorization::skip(fn () => $dbForProject->find('videos_renditions', $queries, 18, 0, [], ['ASC']));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $dbForProject->count('videos_renditions', $queries, APP_LIMIT_COUNT),
|
||||
@@ -527,35 +649,77 @@ App::get('/v1/videos/:videoId/:stream/renditions')
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/:stream/:profile/:fileName')
|
||||
->alias('/v1/videos/:videoId/:stream/:profile/:fileName', [])
|
||||
->desc('Get video playlist manifests')
|
||||
App::delete('/v1/videos/:videoId/renditions/:renditionId')
|
||||
->desc('Delete video subtitle')
|
||||
->groups(['api', 'video'])
|
||||
->label('scope', 'files.write')
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'deleteRendition')
|
||||
->label('sdk.description', '/docs/references/videos/delete-rendition.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('videoId', '', new UID(), 'Video unique ID.')
|
||||
->param('renditionId', '', new UID(), 'Video rendition unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->inject('deviceVideos')
|
||||
->action(function (string $videoId, string $renditionId, Response $response, Database $dbForProject, string $mode, Document $user, Device $deviceVideos) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])]));
|
||||
|
||||
if ($video->isEmpty()) {
|
||||
throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
|
||||
$rendition = Authorization::skip(fn() => $dbForProject->getDocument('videos_renditions', $renditionId));
|
||||
|
||||
if ($rendition->isEmpty()) {
|
||||
throw new Exception('Video rendition not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deleted = $dbForProject->deleteDocument('videos_renditions', $renditionId);
|
||||
|
||||
if (!$deleted) {
|
||||
throw new Exception('Failed to remove video rendition from DB', 500, Exception::GENERAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
Authorization::skip(fn() => $dbForProject->deleteDocument('videos_renditions', $rendition->getId()));
|
||||
if (!empty($rendition['path'])) {
|
||||
$deviceVideos->deletePath($rendition['path']);
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/streams/:streamId')
|
||||
->desc('Get video master renditions manifest')
|
||||
->groups(['api', 'video'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'getPlaylist')
|
||||
->label('sdk.description', '/docs/references/videos/get-playlist.md') // TODO: Create markdown
|
||||
->label('sdk.method', 'getMasterManifest')
|
||||
->label('sdk.description', '/docs/references/videos/get-master-manifest.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
// TODO: Response model
|
||||
->label('scope', 'files.read')
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('stream', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name')
|
||||
->param('profile', '', new Text(18), 'folder name')
|
||||
->param('fileName', '', new Text(128), 'playlist file name')
|
||||
->param('streamId', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('videosDevice')
|
||||
->inject('usage')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->action(function (string $videoId, string $stream, string $profile, string $fileName, Response $response, Database $dbForProject, Device $videosDevice, Stats $usage, string $mode, Document $user) {
|
||||
->action(function (string $videoId, string $streamId, Response $response, Database $dbForProject, string $mode, Document $user) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$videoId])
|
||||
]));
|
||||
|
||||
if (empty($video)) {
|
||||
if ($video->isEmpty()) {
|
||||
throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -565,70 +729,294 @@ App::get('/v1/videos/:videoId/:stream/:profile/:fileName')
|
||||
new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]),
|
||||
new Query('endedAt', Query::TYPE_GREATER, [0]),
|
||||
new Query('status', Query::TYPE_EQUAL, ['ready']),
|
||||
new Query('stream', Query::TYPE_EQUAL, [$stream]),
|
||||
new Query('stream', Query::TYPE_EQUAL, [$streamId]),
|
||||
], 12, 0, [], ['ASC']));
|
||||
|
||||
if (empty($renditions)) {
|
||||
throw new Exception('Renditions not found'); // TODO: Proper error code
|
||||
throw new Exception('Rendition not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$contentType['m3u8'] = 'application/x-mpegurl';
|
||||
$contentType['mpd'] = 'application/dash+xml';
|
||||
$contentType['ts'] = 'video/MP2T';
|
||||
$contentType['m4s'] = 'video/iso.segment';
|
||||
$contentType['vtt'] = 'text/vtt';
|
||||
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
|
||||
$baseUrl = 'http://127.0.0.1/v1/videos/' . $videoId . '/' . $stream . '/' ;
|
||||
$baseUrl = 'http://127.0.0.1/v1/videos/' . $videoId . '/streams/' . $streamId;
|
||||
|
||||
if ($profile === 'master') {
|
||||
if ($stream === 'hls') {
|
||||
$subtitles = Authorization::skip(fn () => $dbForProject->find('videos_subtitles', [new Query('videoId', Query::TYPE_EQUAL, [$video->getId()])], 12, 0, [], ['ASC']));
|
||||
$paramsSubtitles = [];
|
||||
foreach ($subtitles as $subtitle) {
|
||||
$paramsSubtitles[] = [
|
||||
'name' => $subtitle->getAttribute('name'),
|
||||
'code' => $subtitle->getAttribute('code'),
|
||||
'default' => !empty($subtitle->getAttribute('default')) ? 'YES' : 'NO',
|
||||
'uri' => $baseUrl . $subtitle->getAttribute('path') . '/' . $videoId . '_subtitles_' . $subtitle->getAttribute('code') . '.m3u8',
|
||||
];
|
||||
}
|
||||
$paramsRenditions = [];
|
||||
foreach ($renditions as $rendition) {
|
||||
$paramsRenditions[] = [
|
||||
'bandwidth' => ($rendition->getAttribute('videoBitrate') + $rendition->getAttribute('audioBitrate')),
|
||||
'resolution' => $rendition->getAttribute('width') . 'X' . $rendition->getAttribute('height'),
|
||||
'name' => $rendition->getAttribute('name'),
|
||||
'uri' => $baseUrl . $rendition->getAttribute('name') . '/' . $rendition->getAttribute('videoId') . '_' . $rendition->getAttribute('height') . 'p.m3u8',
|
||||
'subs' => !empty($paramsSubtitles) ? ' SUBTITLES="subs"' : '',
|
||||
];
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/hls.phtml');
|
||||
$template->setParam('paramsSubtitles', $paramsSubtitles);
|
||||
$template->setParam('paramsRenditions', $paramsRenditions);
|
||||
$output = $template->render(false);
|
||||
} else {
|
||||
$adaptations = [];
|
||||
foreach ($renditions as $rendition) {
|
||||
$metadata = $rendition->getAttribute('metadata');
|
||||
foreach ($metadata['mpeg-dash']['Period']['AdaptationSet'] as $set) {
|
||||
$adaption = $set['@attributes'];
|
||||
$adaption['baseUrl'] = $baseUrl . $rendition->getAttribute('name') . '/';
|
||||
$adaption['representation'] = $set['Representation']['@attributes'];
|
||||
$adaption['representation']['SegmentTemplate'] = $set['Representation']['SegmentTemplate']['@attributes'];
|
||||
$adaption['representation']['segmentTemplate']['segmentTimeline'] = $set['Representation']['SegmentTemplate']['SegmentTimeline'];
|
||||
$adaptations[] = $adaption;
|
||||
}
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/dash.phtml');
|
||||
$template->setParam('params', $adaptations);
|
||||
$response->setContentType($contentType[$ext]);
|
||||
$output = $template->render(false);
|
||||
if ($streamId === 'hls') {
|
||||
$subtitles = Authorization::skip(fn() => $dbForProject->find('videos_subtitles', [new Query('videoId', Query::TYPE_EQUAL, [$video->getId()])], 12, 0, [], ['ASC']));
|
||||
$paramsSubtitles = [];
|
||||
foreach ($subtitles as $subtitle) {
|
||||
$paramsSubtitles[] = [
|
||||
'name' => $subtitle->getAttribute('name'),
|
||||
'code' => $subtitle->getAttribute('code'),
|
||||
'default' => !empty($subtitle->getAttribute('default')) ? 'YES' : 'NO',
|
||||
'uri' => $baseUrl . '/subtitles/' . $subtitle->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
$paramsRenditions = [];
|
||||
foreach ($renditions as $rendition) {
|
||||
$paramsRenditions[] = [
|
||||
'bandwidth' => ($rendition->getAttribute('videoBitrate') + $rendition->getAttribute('audioBitrate')),
|
||||
'resolution' => $rendition->getAttribute('width') . 'X' . $rendition->getAttribute('height'),
|
||||
'name' => $rendition->getAttribute('name'),
|
||||
'uri' => $baseUrl . '/renditions/' . $rendition->getId(),
|
||||
'subs' => !empty($paramsSubtitles) ? ' SUBTITLES="subs"' : '',
|
||||
];
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/hls-master.phtml');
|
||||
$template->setParam('paramsSubtitles', $paramsSubtitles);
|
||||
$template->setParam('paramsRenditions', $paramsRenditions);
|
||||
$response->setContentType('application/x-mpegurl')->send($template->render(false));
|
||||
} else {
|
||||
$output = $videosDevice->read($videosDevice->getRoot() . '/' . $videoId . '/' . $profile . '/' . $fileName);
|
||||
$adaptations = [];
|
||||
foreach ($renditions as $rendition) {
|
||||
$metadata = $rendition->getAttribute('metadata');
|
||||
foreach ($metadata['mpeg-dash']['Period']['AdaptationSet'] as $set) {
|
||||
$adaption = $set['@attributes'];
|
||||
$adaption['baseUrl'] = $baseUrl . '/renditions/' . $rendition->getId();
|
||||
$adaption['representation'] = $set['Representation']['@attributes'];
|
||||
$adaption['representation']['SegmentTemplate'] = $set['Representation']['SegmentTemplate']['@attributes'];
|
||||
$adaption['representation']['segmentTemplate']['segmentTimeline'] = $set['Representation']['SegmentTemplate']['SegmentTimeline'];
|
||||
$adaptations[] = $adaption;
|
||||
}
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/dash.phtml');
|
||||
$template->setParam('params', $adaptations);
|
||||
$response->setContentType('application/dash+xml')->send($template->render(false));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/streams/:streamId/renditions/:renditionId')
|
||||
->desc('Get video rendition manifest')
|
||||
->groups(['api', 'video'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'getManifest')
|
||||
->label('sdk.description', '/docs/references/videos/get-manifest.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
// TODO: Response model
|
||||
->label('scope', 'files.read')
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('streamId', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name')
|
||||
->param('renditionId', '', new UID(), 'Rendition unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->action(function (string $videoId, string $streamId, string $renditionId, Response $response, Database $dbForProject, string $mode, Document $user) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$videoId])
|
||||
]));
|
||||
|
||||
if (empty($video)) {
|
||||
throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->setContentType($contentType[$ext])->send($output);
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
|
||||
$rendition = Authorization::skip(fn () => $dbForProject->findOne('videos_renditions', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$renditionId]),
|
||||
new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]),
|
||||
new Query('endedAt', Query::TYPE_GREATER, [0]),
|
||||
new Query('status', Query::TYPE_EQUAL, ['ready']),
|
||||
new Query('stream', Query::TYPE_EQUAL, [$streamId]),
|
||||
]));
|
||||
|
||||
if (empty($rendition)) {
|
||||
throw new Exception('Rendition not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$segments = Authorization::skip(fn () => $dbForProject->find('videos_renditions_segments', [
|
||||
new Query('renditionId', Query::TYPE_EQUAL, [$renditionId]),
|
||||
], 4000, 0, [], ['ASC']));
|
||||
|
||||
if (empty($segments)) {
|
||||
throw new Exception('Rendition segments not found', 404, Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$paramsSegments = [];
|
||||
foreach ($segments as $segment) {
|
||||
$paramsSegments[] = [
|
||||
'duration' => $segment->getAttribute('duration'),
|
||||
'url' => 'http://127.0.0.1/v1/videos/' . $videoId . '/streams/' . $streamId . '/renditions/' . $renditionId . '/segments/' . $segment->getId() ,
|
||||
];
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/hls.phtml');
|
||||
$template->setParam('targetDuration', $rendition->getAttribute('targetDuration'));
|
||||
$template->setParam('paramsSegments', $paramsSegments);
|
||||
$response->setContentType('application/x-mpegurl')->send($template->render(false));
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/streams/:streamId/renditions/:renditionId/segments/:segmentId')
|
||||
->desc('Get video rendition segment')
|
||||
->groups(['api', 'video'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'getRenditionSegment')
|
||||
->label('sdk.description', '/docs/references/videos/get-rendition-segment.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
// TODO: Response model
|
||||
->label('scope', 'files.read')
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('streamId', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name')
|
||||
->param('renditionId', '', new UID(), 'Rendition unique ID.')
|
||||
->param('segmentId', '', new UID(), 'Segment unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('videosDevice')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->action(function (string $videoId, string $streamId, string $renditionId, string $segmentId, Response $response, Database $dbForProject, Device $videosDevice, string $mode, Document $user) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$videoId])
|
||||
]));
|
||||
|
||||
if (empty($video)) {
|
||||
throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
|
||||
$rendition = Authorization::skip(fn() => $dbForProject->findOne('videos_renditions', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$renditionId]),
|
||||
new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]),
|
||||
new Query('endedAt', Query::TYPE_GREATER, [0]),
|
||||
new Query('status', Query::TYPE_EQUAL, ['ready']),
|
||||
new Query('stream', Query::TYPE_EQUAL, [$streamId]),
|
||||
]));
|
||||
|
||||
if (empty($rendition)) {
|
||||
throw new Exception('Rendition not found', 404, Exception::VIDEO_RENDITION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$segment = Authorization::skip(fn () => $dbForProject->findOne('videos_renditions_segments', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$segmentId])]));
|
||||
|
||||
if (empty($segment)) {
|
||||
throw new Exception('Rendition segments not found', 404, Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$output = $videosDevice->read($segment->getAttribute('path') . $segment->getAttribute('fileName'));
|
||||
$response->setContentType('video/MP2T')->send($output);
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/streams/:streamId/subtitles/:subtitleId')
|
||||
->desc('Get video subtitle')
|
||||
->groups(['api', 'video'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'getSubtitle')
|
||||
->label('sdk.description', '/docs/references/videos/get-subtitle.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
// TODO: Response model
|
||||
->label('scope', 'files.read')
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('streamId', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name')
|
||||
->param('subtitleId', '', new UID(), 'Subtitle unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->action(function (string $videoId, string $streamId, string $subtitleId, Response $response, Database $dbForProject, string $mode, Document $user) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$videoId])
|
||||
]));
|
||||
|
||||
if (empty($video)) {
|
||||
throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
|
||||
$subtitle = Authorization::skip(fn() => $dbForProject->findOne('videos_subtitles', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$subtitleId]),
|
||||
new Query('status', Query::TYPE_EQUAL, ['ready'])
|
||||
]));
|
||||
|
||||
if (empty($subtitle)) {
|
||||
throw new Exception('subtitle not found', 404, Exception::VIDEO_SUBTITLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$segments = Authorization::skip(fn () => $dbForProject->find('videos_subtitles_segments', [
|
||||
new Query('subtitleId', Query::TYPE_EQUAL, [$subtitleId]),
|
||||
], 4000));
|
||||
|
||||
if (empty($segments)) {
|
||||
throw new Exception('Subtitle segments not found', 404, Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$paramsSegments = [];
|
||||
foreach ($segments as $segment) {
|
||||
$paramsSegments[] = [
|
||||
'duration' => $segment->getAttribute('duration'),
|
||||
'url' => 'http://127.0.0.1/v1/videos/' . $videoId . '/streams/' . $streamId . '/subtitles/' . $subtitleId . '/segments/' . $segment->getId() ,
|
||||
];
|
||||
}
|
||||
|
||||
$template = new View(__DIR__ . '/../../views/videos/hls-subtitles.phtml');
|
||||
$template->setParam('targetDuration', $subtitle->getAttribute('targetDuration'));
|
||||
$template->setParam('paramsSegments', $paramsSegments);
|
||||
$response->setContentType('application/x-mpegurl')->send($template->render(false));
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/videos/:videoId/streams/:streamId/subtitles/:subtitleId/segments/:segmentId')
|
||||
->desc('Get video subtitle segment')
|
||||
->groups(['api', 'video'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'video')
|
||||
->label('sdk.method', 'getSubtitleSegment')
|
||||
->label('sdk.description', '/docs/references/videos/get-subtitle-segment.md') // TODO: Create markdown
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
// TODO: Response model
|
||||
->label('scope', 'files.read')
|
||||
->param('videoId', null, new UID(), 'Video unique ID.')
|
||||
->param('streamId', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name')
|
||||
->param('subtitleId', '', new UID(), 'Subtitle unique ID.')
|
||||
->param('segmentId', '', new UID(), 'Segment unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('videosDevice')
|
||||
->inject('mode')
|
||||
->inject('user')
|
||||
->action(function (string $videoId, string $streamId, string $subtitleId, string $segmentId, Response $response, Database $dbForProject, Device $videosDevice, string $mode, Document $user) {
|
||||
|
||||
$video = Authorization::skip(fn() => $dbForProject->findOne('videos', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$videoId])
|
||||
]));
|
||||
|
||||
if (empty($video)) {
|
||||
throw new Exception('Video not found', 404, Exception::VIDEO_NOT_FOUND);
|
||||
}
|
||||
|
||||
validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode, $user);
|
||||
|
||||
$subtitle = Authorization::skip(fn() => $dbForProject->findOne('videos_subtitles', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$subtitleId]),
|
||||
new Query('status', Query::TYPE_EQUAL, ['ready'])
|
||||
]));
|
||||
|
||||
if (empty($subtitle)) {
|
||||
throw new Exception('subtitle not found', 404, Exception::VIDEO_SUBTITLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$segment = Authorization::skip(fn () => $dbForProject->findOne('videos_subtitles_segments', [
|
||||
new Query('_uid', Query::TYPE_EQUAL, [$segmentId])]));
|
||||
|
||||
if (empty($segment)) {
|
||||
throw new Exception('Subtitle segments not found', 404, Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$output = $videosDevice->read($segment->getAttribute('path') . $segment->getAttribute('fileName'));
|
||||
$response->setContentType('text/vtt')->send($output);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
<?php foreach ($this->getParam('paramsSubtitles', []) as $subtitle): ?>
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="<?php echo $subtitle['name']; ?>",DEFAULT=<?php echo $subtitle['default']; ?>,AUTOSELECT=NO,FORCED=NO,LANGUAGE="<?php echo $subtitle['code']; ?>",URI="<?php echo $subtitle['uri']; ?>"<?php echo PHP_EOL?>
|
||||
<?php endforeach; ?>
|
||||
<?php foreach ($this->getParam('paramsRenditions', []) as $rendition): ?>
|
||||
#EXT-X-STREAM-INF:BANDWIDTH="<?php echo $rendition['bandwidth']; ?>",RESOLUTION="<?php echo $rendition['resolution']; ?>",NAME="<?php echo $rendition['name']; ?>"<?php if($rendition['subs'] !== null): echo $rendition['subs']; endif?><?php echo PHP_EOL?>
|
||||
<?php echo $rendition['uri'] ?><?php echo PHP_EOL?>
|
||||
<?php endforeach; ?>
|
||||
@@ -0,0 +1,10 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:1
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXT-X-TARGETDURATION:<?php echo $this->getParam('targetDuration', 0)?><?php echo PHP_EOL?>
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
<?php foreach ($this->getParam('paramsSegments', []) as $segment): ?>
|
||||
#EXTINF:<?php echo $segment['duration']; ?><?php echo PHP_EOL?>
|
||||
<?php echo $segment['url']; ?><?php echo PHP_EOL?>
|
||||
<?php endforeach; ?>
|
||||
@@ -1,9 +1,10 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
<?php foreach ($this->getParam('paramsSubtitles', []) as $subtitle): ?>
|
||||
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="<?php echo $subtitle['name']; ?>",DEFAULT=<?php echo $subtitle['default']; ?>,AUTOSELECT=NO,FORCED=NO,LANGUAGE="<?php echo $subtitle['code']; ?>",URI="<?php echo $subtitle['uri']; ?>"<?php echo PHP_EOL?>
|
||||
#EXT-X-ALLOW-CACHE:NO
|
||||
#EXT-X-TARGETDURATION:<?php echo $this->getParam('targetDuration', 0)?><?php echo PHP_EOL?>
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
<?php foreach ($this->getParam('paramsSegments', []) as $segment): ?>
|
||||
#EXTINF:<?php echo $segment['duration']; ?><?php echo PHP_EOL?>
|
||||
<?php echo $segment['url']; ?><?php echo PHP_EOL?>
|
||||
<?php endforeach; ?>
|
||||
<?php foreach ($this->getParam('paramsRenditions', []) as $rendition): ?>
|
||||
#EXT-X-STREAM-INF:BANDWIDTH="<?php echo $rendition['bandwidth']; ?>",RESOLUTION="<?php echo $rendition['resolution']; ?>",NAME="<?php echo $rendition['name']; ?>"<?php if($rendition['subs'] !== null): echo $rendition['subs']; endif?><?php echo PHP_EOL?>
|
||||
<?php echo $rendition['uri'] ?><?php echo PHP_EOL?>
|
||||
<?php endforeach; ?>
|
||||
#EXT-X-ENDLIST
|
||||
+83
-69
@@ -7,10 +7,12 @@ use Streaming\Format\StreamFormat;
|
||||
use Streaming\HLSSubtitle;
|
||||
use Streaming\Media;
|
||||
use Streaming\Representation;
|
||||
use Streaming\RepresentationInterface;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use FFMpeg\FFProbe\DataMapping\StreamCollection;
|
||||
@@ -51,7 +53,6 @@ class TranscodingV1 extends Worker
|
||||
|
||||
protected Database $database;
|
||||
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return "Transcoding";
|
||||
@@ -123,7 +124,6 @@ class TranscodingV1 extends Worker
|
||||
];
|
||||
|
||||
$rendition = Authorization::skip(fn() => $this->database->findOne($collection, $queries));
|
||||
|
||||
if (!empty($rendition)) {
|
||||
Authorization::skip(fn() => $this->database->deleteDocument($collection, $rendition->getId()));
|
||||
$deviceFiles = $this->getVideoDevice($project->getId());
|
||||
@@ -198,8 +198,8 @@ class TranscodingV1 extends Worker
|
||||
'path' => $this->inDir . $this->args['videoId'] . '.vtt',
|
||||
];
|
||||
}
|
||||
|
||||
$query = Authorization::skip(function () use ($collection, $profile) {
|
||||
try {
|
||||
$query = Authorization::skip(function () use ($collection, $profile) {
|
||||
return $this->database->createDocument($collection, new Document([
|
||||
'videoId' => $this->args['videoId'],
|
||||
'profileId' => $profile->getId(),
|
||||
@@ -208,7 +208,11 @@ class TranscodingV1 extends Worker
|
||||
'status' => self::STATUS_START,
|
||||
'stream' => $profile['stream'],
|
||||
]));
|
||||
});
|
||||
});
|
||||
} catch (DuplicateException $exception) {
|
||||
throw new Exception('Video rendition already exists', 409, Exception::VIDEO_RENDITION_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$representation = (new Representation())->
|
||||
@@ -239,16 +243,42 @@ class TranscodingV1 extends Worker
|
||||
}
|
||||
}
|
||||
|
||||
if ($profile['stream'] === 'hls') {
|
||||
$m3u8 = $this->parseM3u8($this->outPath . '_' . $representation->getHeight() . 'p.m3u8');
|
||||
foreach ($m3u8['segments'] as $segment) {
|
||||
Authorization::skip(function () use ($segment, $project, $query) {
|
||||
return $this->database->createDocument('videos_renditions_segments', new Document([
|
||||
'renditionId' => $query->getId(),
|
||||
'fileName' => $segment['fileName'],
|
||||
'path' => $this->getVideoDevice($project->getId())->getPath($this->args['videoId']) . '/' . $this->getRenditionName() . '/',
|
||||
'duration' => $segment['duration'],
|
||||
]));
|
||||
});
|
||||
}
|
||||
$query->setAttribute('targetDuration', $m3u8['targetDuration']);
|
||||
}
|
||||
|
||||
$query->setAttribute('status', self::STATUS_END);
|
||||
$query->setAttribute('endedAt', time());
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
Authorization::skip(fn() => $this->database->updateDocument($collection, $query->getId(), $query));
|
||||
|
||||
if (!empty($subtitles)) {
|
||||
foreach ($subtitles as $subtitle) {
|
||||
if ($profile['stream'] === 'hls') {
|
||||
$m3u8 = $this->parseM3u8($this->outPath . '_subtitles_' . $subtitle['code'] . '.m3u8');
|
||||
foreach ($m3u8['segments'] as $segment) {
|
||||
Authorization::skip(function () use ($segment, $project, $subtitle) {
|
||||
return $this->database->createDocument('videos_subtitles_segments', new Document([
|
||||
'subtitleId' => $subtitle->getId(),
|
||||
'fileName' => $segment['fileName'],
|
||||
'path' => $this->getVideoDevice($project->getId())->getPath($this->args['videoId']) . '/' ,
|
||||
'duration' => $segment['duration'],
|
||||
]));
|
||||
});
|
||||
}
|
||||
$subtitle->setAttribute('targetDuration', $m3u8['targetDuration']);
|
||||
}
|
||||
|
||||
$subtitle->setAttribute('status', self::STATUS_READY);
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
'videos_subtitles',
|
||||
@@ -262,31 +292,23 @@ class TranscodingV1 extends Worker
|
||||
$fileNames = scandir($this->outDir);
|
||||
|
||||
foreach ($fileNames as $fileName) {
|
||||
if (
|
||||
$fileName === '.' ||
|
||||
$fileName === '..' ||
|
||||
str_contains($fileName, '.json')
|
||||
) {
|
||||
if ($fileName === '.' || $fileName === '..') {
|
||||
//str_contains($fileName, '.json')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deviceFiles = $this->getVideoDevice($project->getId());
|
||||
$devicePath = $deviceFiles->getPath($this->args['videoId']);
|
||||
$devicePath = $this->getVideoDevice($project->getId())->getPath($this->args['videoId']);
|
||||
$data = $this->getFilesDevice($project->getId())->read($this->outDir . $fileName);
|
||||
$to = $devicePath . '/' . $this->getRenditionName() . '/';
|
||||
// if (str_contains($fileName, "_subtitles_") || str_contains($fileName, ".vtt")) {
|
||||
// $to = $devicePath . '/';
|
||||
// }
|
||||
if (str_contains($fileName, "_subtitles_") || str_contains($fileName, ".vtt")) {
|
||||
$to = $devicePath . '/';
|
||||
}
|
||||
|
||||
$this->getVideoDevice($project->getId())->write($to . $fileName, $data, \mime_content_type($this->outDir . $fileName));
|
||||
if ($start === 0) {
|
||||
$query->setAttribute('status', self::STATUS_UPLOADING);
|
||||
$query->setAttribute('path', $devicePath . '/' . $this->getRenditionName());
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
Authorization::skip(fn() => $this->database->updateDocument($collection, $query->getId(), $query));
|
||||
$start = 1;
|
||||
}
|
||||
|
||||
@@ -302,23 +324,15 @@ class TranscodingV1 extends Worker
|
||||
}
|
||||
|
||||
$query->setAttribute('status', self::STATUS_READY);
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
Authorization::skip(fn() => $this->database->updateDocument($collection, $query->getId(), $query));
|
||||
} catch (\Throwable $th) {
|
||||
$query->setAttribute('metadata', json_encode([
|
||||
'code' => $th->getCode(),
|
||||
'message' => $th->getMessage(),
|
||||
'message' => substr($th->getMessage(), 0, 2048),
|
||||
]));
|
||||
|
||||
$query->setAttribute('status', self::STATUS_ERROR);
|
||||
Authorization::skip(fn() => $this->database->updateDocument(
|
||||
$collection,
|
||||
$query->getId(),
|
||||
$query
|
||||
));
|
||||
Authorization::skip(fn() => $this->database->updateDocument($collection, $query->getId(), $query));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,9 +367,8 @@ class TranscodingV1 extends Worker
|
||||
file_get_contents($this->outDir . $this->args['videoId'] . '.mpd')
|
||||
);
|
||||
|
||||
$general = $this->getVideoStreamInfo($dash->metadata()->export());
|
||||
$general['width'] = $representation->getWidth();
|
||||
$general['height'] = $representation->getHeight();
|
||||
$general = $this->getVideoStreamInfo($dash->metadata()->export(), $representation);
|
||||
|
||||
return [
|
||||
$general,
|
||||
['mpeg-dash' => !empty($xml) ? json_decode(json_encode((array)$xml), true) : []],
|
||||
@@ -376,20 +389,10 @@ class TranscodingV1 extends Worker
|
||||
->setHlsAllowCache(false)
|
||||
->addRepresentation($representation)
|
||||
->setAdditionalParams($additionalParams)
|
||||
->setHlsBaseUrl($this->getHlsBaseUri())
|
||||
//->setHlsBaseUrl()
|
||||
->save($this->outPath);
|
||||
|
||||
|
||||
$general = $this->getVideoStreamInfo($hls->metadata()->export());
|
||||
var_dump($general);
|
||||
$general['videoBitrate'] = $representation->getKiloBitrate() * 1024;
|
||||
$general['audioBitrate'] = $representation->getAudioKiloBitrate() * 1024;
|
||||
$general['width'] = $representation->getWidth();
|
||||
$general['height'] = $representation->getHeight();
|
||||
|
||||
foreach ($subtitles as $subtitle) {
|
||||
$this->rewriteHlsLines($this->outPath . '_subtitles_' . $subtitle['code'] . '.m3u8');
|
||||
}
|
||||
$general = $this->getVideoStreamInfo($hls->metadata()->export(), $representation);
|
||||
|
||||
return [
|
||||
$general, []
|
||||
@@ -402,6 +405,7 @@ class TranscodingV1 extends Worker
|
||||
*/
|
||||
private function getHlsBaseUri(bool $nest = true): string
|
||||
{
|
||||
|
||||
$uri = self::BASE_URL_ENDPOINT . '/' . $this->args['videoId'] . '/' . self::STREAM_HLS . '/';
|
||||
|
||||
if (empty($nest)) {
|
||||
@@ -411,27 +415,36 @@ class TranscodingV1 extends Worker
|
||||
return $uri . $this->getRenditionName() . '/';
|
||||
}
|
||||
|
||||
private function rewriteHlsLines($path)
|
||||
private function parseM3u8(string $path): array
|
||||
{
|
||||
$segments = [];
|
||||
$targetDuration = 0;
|
||||
$handle = fopen($path, "r");
|
||||
$destination = fopen($path . '_tmp', "w");
|
||||
if ($handle) {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$newLine = str_replace(array("\r","\n"), "", $line);
|
||||
if (
|
||||
str_contains($line, ".ts") ||
|
||||
str_contains($line, ".vtt") ||
|
||||
str_contains($line, ".m3u8")
|
||||
) {
|
||||
//$newLine = $this->getHlsBaseUri(str_contains($line, ".vtt") ?? false) . $newLine;
|
||||
$newLine = $this->getHlsBaseUri() . $newLine;
|
||||
$line = str_replace([",","\r","\n"], "", $line);
|
||||
if (str_contains($line, "#EXT-X-TARGETDURATION")) {
|
||||
$targetDuration = str_replace(["#EXT-X-TARGETDURATION:"], "", $line);
|
||||
}
|
||||
if (str_contains($line, "#EXTINF")) {
|
||||
$duration = str_replace(["#EXTINF:"], "", $line);
|
||||
}
|
||||
if (str_contains($line, ".ts") || str_contains($line, ".vtt")) {
|
||||
if (!empty($duration)) {
|
||||
$segments[] = [
|
||||
'fileName' => $line,
|
||||
'duration' => $duration
|
||||
];
|
||||
$duration = null;
|
||||
}
|
||||
}
|
||||
fwrite($destination, $newLine . PHP_EOL);
|
||||
}
|
||||
rename($path . '_tmp', $path);
|
||||
fclose($handle);
|
||||
fclose($destination);
|
||||
}
|
||||
return [
|
||||
'targetDuration' => $targetDuration,
|
||||
'segments' => $segments
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -457,25 +470,26 @@ class TranscodingV1 extends Worker
|
||||
* @param $metadata array
|
||||
* @return array
|
||||
*/
|
||||
private function getVideoStreamInfo(array $metadata): array
|
||||
private function getVideoStreamInfo(array $metadata, RepresentationInterface $representation): array
|
||||
{
|
||||
$info = [];
|
||||
// if (!empty($metadata['stream']['resolutions'][0])) {
|
||||
// $general = $metadata['stream']['resolutions'][0];
|
||||
// $info['resolution'] = $general['dimension'];
|
||||
// }
|
||||
|
||||
$info['width'] = $representation->getWidth();
|
||||
$info['height'] = $representation->getHeight();
|
||||
if (!empty($metadata['video']['streams'])) {
|
||||
foreach ($metadata['video']['streams'] as $streams) {
|
||||
if ($streams['codec_type'] === 'video') {
|
||||
$info['duration'] = !empty($streams['duration']) ? $streams['duration'] : '0';
|
||||
$info['videoCodec'] = !empty($streams['codec_name']) ? $streams['codec_name'] . ',' . $streams['codec_tag_string'] : '';
|
||||
$info['videoBitrate'] = !empty($streams['bit_rate']) ? (int)$streams['bit_rate'] : 0;
|
||||
$info['videoBitrate'] = !empty($streams['bit_rate']) ? (int)$streams['bit_rate'] : $representation->getKiloBitrate() * 1024;
|
||||
$info['videoFramerate'] = !empty($streams['avg_frame_rate']) ? $streams['avg_frame_rate'] : '';
|
||||
} elseif ($streams['codec_type'] === 'audio') {
|
||||
$info['audioCodec'] = !empty($streams['codec_name']) ? $streams['codec_name'] . ',' . $streams['codec_tag_string'] : '' ;
|
||||
$info['audioBitrate'] = !empty($streams['sample_rate']) ? (int)$streams['sample_rate'] : 0;
|
||||
$info['audioBitrate'] = !empty($streams['bit_rate']) ? (int)$streams['bit_rate'] : 0;
|
||||
$info['audioSamplerate'] = !empty($streams['sample_rate']) ? (int)$streams['sample_rate'] : 0;
|
||||
$info['audioBitrate'] = !empty($streams['bit_rate']) ? (int)$streams['bit_rate'] : $representation->getAudioKiloBitrate() * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -41,7 +41,7 @@
|
||||
"ext-zlib": "*",
|
||||
"ext-sockets": "*",
|
||||
"appwrite/php-clamav": "1.1.*",
|
||||
"appwrite/php-runtimes": "0.10.*",
|
||||
"appwrite/php-runtimes": "0.9.*",
|
||||
"utopia-php/framework": "0.19.*",
|
||||
"utopia-php/logger": "0.3.*",
|
||||
"utopia-php/abuse": "0.7.*",
|
||||
@@ -50,7 +50,7 @@
|
||||
"utopia-php/cache": "0.6.*",
|
||||
"utopia-php/cli": "0.12.*",
|
||||
"utopia-php/config": "0.2.*",
|
||||
"utopia-php/database": "0.18.*",
|
||||
"utopia-php/database": "0.17.*",
|
||||
"utopia-php/locale": "0.4.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
@@ -80,9 +80,8 @@
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"appwrite/sdk-generator": "0.19.5",
|
||||
"appwrite/sdk-generator": "0.18.8",
|
||||
"phpunit/phpunit": "9.5.20",
|
||||
"squizlabs/php_codesniffer": "^3.6",
|
||||
"swoole/ide-helper": "4.8.9",
|
||||
"textalk/websocket": "1.5.7"
|
||||
},
|
||||
|
||||
@@ -171,9 +171,15 @@ class Exception extends \Exception
|
||||
public const DOMAIN_VERIFICATION_FAILED = 'domain_verification_failed';
|
||||
|
||||
/** Video */
|
||||
public const VIDEO_PROFILE_NOT_FOUND = 'profile_not_found';
|
||||
public const VIDEO_PROFILE_NOT_FOUND = 'video_profile_not_found';
|
||||
public const VIDEO_PROFILE_ALREADY_EXISTS = 'video_profile_already_exists';
|
||||
public const VIDEO_RENDITION_NOT_FOUND = 'video_rendition_not_found';
|
||||
public const VIDEO_RENDITION_ALREADY_EXISTS = 'video_rendition_already_exists';
|
||||
public const VIDEO_SUBTITLE_NOT_FOUND = 'video_subtitle_not_found';
|
||||
public const VIDEO_NOT_FOUND = 'video_not_found';
|
||||
public const VIDEO_SUBTITLE_NOT_FOUND = 'subtitle_not_found';
|
||||
public const VIDEO_RENDITION_SEGMENT_NOT_FOUND = 'video_rendition_segment_not__found';
|
||||
public const VIDEO_SUBTITLE_SEGMENT_NOT_FOUND = 'video_subtitle_segment_not_found';
|
||||
|
||||
|
||||
|
||||
private $type = '';
|
||||
|
||||
@@ -30,9 +30,72 @@ class Video extends Model
|
||||
])
|
||||
->addRule('size', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'File SIZE.',
|
||||
'description' => 'File size.',
|
||||
'default' => '',
|
||||
'example' => 3567790,
|
||||
'example' => 23647142,
|
||||
])
|
||||
->addRule('duration', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'require' => false,
|
||||
'description' => 'Video duration.',
|
||||
'default' => 0,
|
||||
'example' => 92.739989,
|
||||
])
|
||||
->addRule('width', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'require' => false,
|
||||
'description' => 'Video width.',
|
||||
'default' => 0,
|
||||
'example' => 960,
|
||||
])
|
||||
->addRule('height', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'require' => false,
|
||||
'description' => 'Video height.',
|
||||
'default' => 0,
|
||||
'example' => 544,
|
||||
])
|
||||
->addRule('videoCodec', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'require' => false,
|
||||
'description' => 'Video codec.',
|
||||
'default' => null,
|
||||
'example' => 'h264,avc1',
|
||||
])
|
||||
->addRule('videoBitrate', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'require' => false,
|
||||
'description' => 'Video bitrate.',
|
||||
'default' => 0,
|
||||
'example' => 564790,
|
||||
])
|
||||
->addRule('videoFramerate', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'require' => false,
|
||||
'description' => 'Video frame rate.',
|
||||
'default' => 0,
|
||||
'example' => '231947377/4638947',
|
||||
])
|
||||
->addRule('audioCodec', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'require' => false,
|
||||
'description' => 'Audio codec.',
|
||||
'default' => null,
|
||||
'example' => 'aac,mp4a',
|
||||
])
|
||||
->addRule('audioBitrate', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'require' => false,
|
||||
'description' => 'Audio bitrate.',
|
||||
'default' => 0,
|
||||
'example' => 127999,
|
||||
])
|
||||
->addRule('audioSamplerate', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'require' => false,
|
||||
'description' => 'Audio sample rate.',
|
||||
'default' => '0',
|
||||
'example' => '',
|
||||
])
|
||||
;
|
||||
}
|
||||
@@ -44,7 +107,7 @@ class Video extends Model
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Video entity';
|
||||
return 'Video';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,25 +13,25 @@ class VideoRendition extends Model
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'ID.',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('videoId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Video ID.',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('profileId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'profile ID.',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => 'd5fg5ehg1c168g7c',
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Rendition name.',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => '720P',
|
||||
])
|
||||
->addRule('startedAt', [
|
||||
@@ -49,7 +49,7 @@ class VideoRendition extends Model
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Rendition transcoding status',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => 'ready',
|
||||
])
|
||||
->addRule('progress', [
|
||||
@@ -61,9 +61,64 @@ class VideoRendition extends Model
|
||||
->addRule('stream', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Rendition trascoding stream protocol type',
|
||||
'default' => 0,
|
||||
'example' => 88,
|
||||
'default' => null,
|
||||
'example' => 'hls',
|
||||
])
|
||||
->addRule('duration', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Video duration.',
|
||||
'default' => 0,
|
||||
'example' => '92.739989',
|
||||
])
|
||||
->addRule('width', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Video width.',
|
||||
'default' => 0,
|
||||
'example' => 300,
|
||||
])
|
||||
->addRule('height', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Video height.',
|
||||
'default' => 0,
|
||||
'example' => 400,
|
||||
])
|
||||
->addRule('videoCodec', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Video codec.',
|
||||
'default' => null,
|
||||
'example' => 'h264,avc1',
|
||||
])
|
||||
->addRule('videoBitrate', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Video bitrate.',
|
||||
'default' => 0,
|
||||
'example' => 564790,
|
||||
])
|
||||
->addRule('videoFramerate', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Video frame rate.',
|
||||
'default' => 0,
|
||||
'example' => '231947377/4638947',
|
||||
])
|
||||
->addRule('audioCodec', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Audio codec.',
|
||||
'default' => null,
|
||||
'example' => 'aac,mp4a',
|
||||
])
|
||||
->addRule('audioBitrate', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Audio bitrate.',
|
||||
'default' => 0,
|
||||
'example' => 127999,
|
||||
])
|
||||
->addRule('audioSamplerate', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Audio sample rate.',
|
||||
'default' => 0,
|
||||
'example' => 44100,
|
||||
])
|
||||
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,44 +13,44 @@ class VideoProfile extends Model
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'ID.',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Video profile name.',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => '360P',
|
||||
])
|
||||
->addRule('videoBitrate', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Video bitrate.',
|
||||
'default' => '',
|
||||
'default' => 0,
|
||||
'example' => 3,
|
||||
])
|
||||
->addRule('audioBitrate', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Audio bitrate.',
|
||||
'default' => '',
|
||||
'default' => 0,
|
||||
'example' => 3,
|
||||
])
|
||||
->addRule('width', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Video width.',
|
||||
'default' => '',
|
||||
'default' => 0,
|
||||
'example' => 300,
|
||||
])
|
||||
->addRule('height', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Video height.',
|
||||
'default' => '',
|
||||
'default' => 0,
|
||||
'example' => 400,
|
||||
])
|
||||
|
||||
->addRule('stream', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'http video stream type.',
|
||||
'default' => '',
|
||||
'default' => null,
|
||||
'example' => 'mpeg-dash',
|
||||
])
|
||||
;
|
||||
|
||||
@@ -22,12 +22,12 @@ class VideoCustomServerTest extends Scope
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'name' => 'My test profile',
|
||||
'videoBitrate' => 570,
|
||||
'audioBitrate' => 120,
|
||||
'width' => 600,
|
||||
'height' => 400,
|
||||
'stream' => 'hls',
|
||||
'name' => 'My test profile',
|
||||
'videoBitrate' => 570,
|
||||
'audioBitrate' => 120,
|
||||
'width' => 600,
|
||||
'height' => 400,
|
||||
'stream' => 'hls',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
@@ -39,12 +39,12 @@ class VideoCustomServerTest extends Scope
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'name' => 'My updated test profile',
|
||||
'videoBitrate' => 590,
|
||||
'audioBitrate' => 120,
|
||||
'width' => 300,
|
||||
'height' => 400,
|
||||
'stream' => 'hls',
|
||||
'name' => 'My updated test profile',
|
||||
'videoBitrate' => 590,
|
||||
'audioBitrate' => 120,
|
||||
'width' => 300,
|
||||
'height' => 400,
|
||||
'stream' => 'hls',
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -54,7 +54,7 @@ class VideoCustomServerTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/profiles/' . $response['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$profileId = $response['body']['$id'];
|
||||
@@ -67,7 +67,7 @@ class VideoCustomServerTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/videos/profiles/' . $response['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
@@ -75,7 +75,7 @@ class VideoCustomServerTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/profiles/' . $profileId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
@@ -85,7 +85,7 @@ class VideoCustomServerTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -126,16 +126,16 @@ class VideoCustomServerTest extends Scope
|
||||
], [
|
||||
'bucketId' => $this->getBucket()['$id'],
|
||||
'fileId' => $this->getSubtitle()['$id'],
|
||||
'name' => 'English',
|
||||
'code' => 'Eng',
|
||||
'default' => true,
|
||||
'name' => 'English',
|
||||
'code' => 'Eng',
|
||||
'default' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/'. $videoId . '/subtitles' , [
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/subtitles', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -154,28 +154,45 @@ class VideoCustomServerTest extends Scope
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'bucketId' => $this->getBucket()['$id'],
|
||||
'fileId' => $this->getSubtitle()['$id'],
|
||||
'name' => 'Polish',
|
||||
'code' => 'Pol',
|
||||
'default' => false,
|
||||
'fileId' => $this->getSubtitle()['$id'],
|
||||
'name' => 'Polish',
|
||||
'code' => 'Pol',
|
||||
'default' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/'. $videoId . '/subtitles' , [
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/subtitles', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertEquals(1, $response['body']['total']);
|
||||
$this->assertNotEmpty($response['body']['subtitles']);
|
||||
$this->assertNotEmpty($response['body']['subtitles'][0]['$id']);
|
||||
$this->assertEquals('Polish', $response['body']['subtitles'][0]['name']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/videos/' . $videoId . '/subtitles/' . $response['body']['subtitles'][0]['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/subtitles', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
|
||||
@@ -203,8 +220,8 @@ class VideoCustomServerTest extends Scope
|
||||
], [
|
||||
'bucketId' => $this->getBucket()['$id'],
|
||||
'fileId' => $this->getSubtitle()['$id'],
|
||||
'name' => 'hebrew',
|
||||
'code' => 'heb',
|
||||
'name' => 'hebrew',
|
||||
'code' => 'heb',
|
||||
]);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/subtitles', [
|
||||
@@ -214,15 +231,17 @@ class VideoCustomServerTest extends Scope
|
||||
], [
|
||||
'bucketId' => $this->getBucket()['$id'],
|
||||
'fileId' => $this->getSubtitle()['$id'],
|
||||
'name' => 'english',
|
||||
'code' => 'eng',
|
||||
'default' => true,
|
||||
'name' => 'english',
|
||||
'code' => 'eng',
|
||||
'default' => true,
|
||||
]);
|
||||
|
||||
$subtitleId = $response['body']['$id'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/profiles', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
|
||||
@@ -235,11 +254,12 @@ class VideoCustomServerTest extends Scope
|
||||
'profileId' => $profileId,
|
||||
]);
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
|
||||
return [
|
||||
'videoId' => $videoId,
|
||||
'stream' => ''
|
||||
];
|
||||
'videoId' => $videoId,
|
||||
'subtitleId' => $subtitleId
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -264,7 +284,6 @@ class VideoCustomServerTest extends Scope
|
||||
|
||||
|
||||
foreach ($response['body']['profiles'] as $profile) {
|
||||
|
||||
$profileId = $profile['$id'];
|
||||
$response = $this->client->call(Client::METHOD_POST, '/videos/' . $videoId . '/rendition', [
|
||||
'content-type' => 'application/json',
|
||||
@@ -273,7 +292,7 @@ class VideoCustomServerTest extends Scope
|
||||
], [
|
||||
'profileId' => $profileId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'videoId' => $videoId,
|
||||
@@ -283,12 +302,12 @@ class VideoCustomServerTest extends Scope
|
||||
/**
|
||||
* @depends testTranscodeWithSubs
|
||||
*/
|
||||
public function testGetRendition(array $data): array
|
||||
public function testGetRenditions(array $data): array
|
||||
{
|
||||
|
||||
sleep(30);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/hls/renditions', [
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/renditions', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -297,16 +316,29 @@ class VideoCustomServerTest extends Scope
|
||||
$this->assertNotEmpty($response['body']['renditions']);
|
||||
|
||||
$videoId = $response['body']['renditions'][0]['videoId'];
|
||||
$renditionId = $response['body']['renditions'][0]['$id'];
|
||||
$profileId = $response['body']['renditions'][0]['profileId'];
|
||||
$profileName = $response['body']['renditions'][0]['name'];
|
||||
$stream = $response['body']['renditions'][0]['stream'];
|
||||
|
||||
var_dump($response['body']);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/rendition/' . $renditionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals($renditionId, $response['body']['$id']);
|
||||
var_dump($response);
|
||||
|
||||
return [
|
||||
'renditionId' => $renditionId,
|
||||
'videoId' => $videoId,
|
||||
'profileId' => $profileId,
|
||||
'profileName' => $profileName,
|
||||
'subtitleId' => $data['subtitleId'],
|
||||
'stream' => $stream
|
||||
];
|
||||
}
|
||||
@@ -318,7 +350,7 @@ class VideoCustomServerTest extends Scope
|
||||
{
|
||||
sleep(20);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/' . $data['stream'] . '/master/' . $data['videoId'] . '.m3u8', [
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/streams/' . $data['stream'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -326,7 +358,7 @@ class VideoCustomServerTest extends Scope
|
||||
|
||||
var_dump($response['body']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/' . $data['stream'] . '/' . $data['profileName'] . '/' . $data['videoId'] . '_360p.m3u8', [
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . $data['stream'] . '/renditions/' . $data['renditionId'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -334,7 +366,32 @@ class VideoCustomServerTest extends Scope
|
||||
|
||||
var_dump($response['body']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/' . $data['stream'] . '/' . $data['profileName'] . '/' . $data['videoId'] . '_360p_0000.ts', [
|
||||
preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match);
|
||||
|
||||
$segmentUrl = $match[0][0];
|
||||
$response = $this->client->call(Client::METHOD_GET, $segmentUrl, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
//var_dump($response['body']);
|
||||
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/streams/' . $data['stream'] . '/subtitles/' . $data['subtitleId'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
var_dump($response['body']);
|
||||
|
||||
preg_match_all('#\b/videos[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $response['body'], $match);
|
||||
|
||||
$segmentUrl = $match[0][0];
|
||||
|
||||
var_dump($segmentUrl);
|
||||
$response = $this->client->call(Client::METHOD_GET, $segmentUrl, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -342,20 +399,4 @@ class VideoCustomServerTest extends Scope
|
||||
|
||||
var_dump($response['body']);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @depends testTranscodingRendition
|
||||
// */
|
||||
// public function testDashStreamRender($data): void
|
||||
// {
|
||||
// sleep(20);
|
||||
//
|
||||
// $response = $this->client->call(Client::METHOD_GET, '/videos/' . $data['videoId'] . '/mpeg-dash/master/' . $data['videoId'] . '.mpd', [
|
||||
// 'content-type' => 'application/json',
|
||||
// 'x-appwrite-project' => $this->getProject()['$id'],
|
||||
// 'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
// ]);
|
||||
//
|
||||
// var_dump($response['body']);
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user