hls stream

This commit is contained in:
shimon
2022-07-19 10:51:25 +03:00
parent cb0ab3c823
commit fbe63a884e
21 changed files with 1049 additions and 315 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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',
+5
View File
@@ -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
View File
@@ -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);
});
+9
View File
@@ -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; ?>
+10
View File
@@ -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; ?>
+7 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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"
},
+8 -2
View File
@@ -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 = '';
+66 -3
View File
@@ -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']);
// }
}