Commit 9ebb0a19 authored by Mark Harding's avatar Mark Harding

(feat): transcoder to support public endpoints, cli and fallback for legacy

1 merge request!414WIP: New transcoder
Pipeline #101949165 passed with stages
in 6 minutes and 57 seconds
......@@ -21,9 +21,14 @@ class Transcode extends Cli\Controller implements Interfaces\CliControllerInterf
public function exec()
{
$transcoder = new Core\Media\Services\FFMpeg;
$transcoder->setKey($this->getOpt('guid'));
$transcoder->setFullHD($this->getOpt('full_hd') ?? false);
$transcoder->onQueue();
$entity = Di::_()->get('EntitiesBuilder')->single($this->getOpt('guid'));
if (!$entity) {
$this->out('Entity not found');
return;
}
$manager = Di::_()->get('Media\Video\Transcoder\Manager');
$manager->createTranscodes($entity);
}
}
<?php
/**
* Minds Video Controller
*
* @author Mark Harding
*/
namespace Minds\Controllers\api\v2\media;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Media\Proxy\Download;
use Minds\Core\Media\Proxy\Resize;
use Minds\Interfaces;
class video implements Interfaces\Api, Interfaces\ApiIgnorePam
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
$videoManager = Di::_()->get('Media\Video\Manager');
$video = $videoManager->get($pages[0]);
Factory::response([
'entity' => $video->export(),
'sources' => Factory::exportable($videoManager->getSources($video)),
'poster' => $video->getIconUrl(),
]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
*/
public function post($pages)
{
http_response_code(501);
exit;
}
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @return mixed|null
*/
public function put($pages)
{
http_response_code(501);
exit;
}
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
{
http_response_code(501);
exit;
}
}
......@@ -11,17 +11,29 @@ use Minds\Core\Di\Di;
use Minds\Entities\Activity;
use Minds\Entities\Entity;
use Minds\Entities\Video;
use Minds\Core\EntitiesBuilder;
use Minds\Common\Repository\Response;
class Manager
{
/** @var Config $config */
/** @var Config */
private $config;
/** @var S3Client $s3 */
/** @var S3Client */
private $s3;
public function __construct($config = null, $s3 = null)
{
/** @var EntitiesBuilder */
private $entitiesBuilder;
/** @var Transcoder\Manager */
private $transcoderManager;
public function __construct(
$config = null,
$s3 = null,
$entitiesBuilder = null,
$transcoderManager = null
) {
$this->config = $config ?? Di::_()->get('Config');
// AWS
......@@ -36,6 +48,63 @@ class Manager
];
}
$this->s3 = $s3 ?: new S3Client(array_merge(['version' => '2006-03-01'], $opts));
$this->entitiesBuilder = $entitiesBuilder ?? Di::_()->get('EntitiesBuilder');
$this->transcoderManager = $transcoderManager ?? Di::_()->get('Media\Video\Transcoder\Manager');
}
/**
* Return a video
* @param string $guid
* @return Video
*/
public function get($guid): Video
{
return $this->entitiesBuilder->single($guid);
}
/**
* Return transcodes
* @param Video $video
* @return Source[]
*/
public function getSources(Video $video): array
{
$transcodes = $this->transcoderManager->getList([
'guid' => $video->getGuid(),
'legacyPolyfill' => true,
]);
$sources = [];
foreach ($transcodes as $transcode) {
if ($transcode->getStatus() != 'completed') {
continue;
}
if ($transcode->getProfile() instanceof Transcoder\TranscodeProfiles\Thumbnails) {
continue;
}
$source = new Source();
$source
->setGuid($transcode->getGuid())
->setType($transcode->getProfile()->getFormat())
->setLabel($transcode->getProfile()->getId())
->setSize($transcode->getProfile()->getHeight())
->setSrc(implode('/', [
$this->config->get('transcoder')['cdn_url'] ?? 'https://cdn-cinemr.minds.com/cinemr_dev',
$transcode->getGuid(),
$transcode->getProfile()->getStorageName()
]));
$sources[] = $source;
}
// Sort the array so that mp4's are first
usort($sources, function ($a, $b) {
if ($a->getType() === 'video/mp4') {
return -1;
}
return 1;
});
return $sources;
}
/**
......
<?php
namespace Minds\Core\Media\Video;
use Minds\Traits\MagicAttributes;
/**
* @method string getGuid()
* @method Source setGuid(string $guid)
* @method string getSrc()
* @method Source setSrc(string $src)
* @method string getType()
* @method Source setType(string $type)
* @method int getSize()
* @method Source setSize(int $size)
* @method string getLabel()
* @method Source setLabel(string $label)
*/
class Source
{
use MagicAttributes;
/** @var string */
protected $guid;
/** @var string */
protected $src;
/** @var string */
protected $type;
/** @var int */
protected $size;
/** @var string */
protected $label;
/**
* Export source
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'guid' => $this->guid,
'src' => $this->src,
'type' => $this->type,
'size' => $this->size,
'label' => $this->label,
];
}
}
......@@ -8,6 +8,8 @@ use Minds\Core\Media\Video\Transcoder\Delegates\QueueDelegate;
use Minds\Core\Media\Video\Transcoder\Delegates\NotificationDelegate;
use Minds\Entities\Video;
use Minds\Traits\MagicAttributes;
use Minds\Common\Repository\Response;
use Minds\Core\Media\Video\Source;
class Manager
{
......@@ -53,15 +55,53 @@ class Manager
* Return a list of transcodes
* @return Response
*/
public function getList($opts): Response
public function getList($opts): ?Response
{
$opts = array_merge([
'guid' => null,
'profileId' => null,
'status' => null,
'legacyPolyfill' => false,
], $opts);
return $this->repository->getList($opts);
$response = $this->repository->getList($opts);
if ($opts['legacyPolyfill'] && !$response->count()) {
$response = $this->getLegacyPolyfill($opts);
}
return $response;
}
/**
* Return a list of legacy transcodes by reading from storage
* @param array
* @return Response
*/
private function getLegacyPolyfill(array $opts): ?Response
{
$files = $this->transcodeStorage->ls($opts['guid']);
if (!$files) {
return null;
}
$response = new Response();
foreach ($files as $fileName) {
// Loop through each profile to see if fileName is a match
foreach (self::TRANSCODE_PROFILES as $profile) {
$profile = new $profile();
if ($profile->getStorageName() && strpos($fileName, $profile->getStorageName()) !== false) {
$transcode = new Transcode();
$transcode
->setGuid($opts['guid'])
->setProfile($profile)
->setStatus(TranscodeStates::COMPLETED);
$response[] = $transcode;
}
}
}
return $response;
}
/**
......
......@@ -133,7 +133,7 @@ class Repository
*/
public function add(Transcode $transcode): bool
{
$statement = "INSERT INTO video_transcodes (guid, profile_id, status) VALUES (?, ?)";
$statement = "INSERT INTO video_transcodes (guid, profile_id, status) VALUES (?, ?, ?)";
$values = [
new Bigint($transcode->getGuid()),
$transcode->getProfile()->getId(),
......@@ -252,7 +252,7 @@ class Repository
->setProfile(TranscodeProfiles\Factory::build((string) $row['profile_id']))
->setProgress($row['progress'])
->setStatus($row['status'])
->setLastEventTimestampMs(round($row['last_event_timestamp_ms']->microtime(true) * 1000))
->setLastEventTimestampMs($row['last_event_timestamp_ms'] ? round($row['last_event_timestamp_ms']->microtime(true) * 1000) : null)
->setLengthSecs($row['length_secs'])
->setBytes($row['bytes']);
return $transcode;
......
......@@ -64,7 +64,8 @@ class FFMpegExecutor implements TranscodeExecutorInterface
// Prepare the source of this transcode
$source = new Transcode();
$source->setProfile(new TranscodeProfiles\Source()); // Simply change the source
$source->setGuid($transcode->getGuid())
->setProfile(new TranscodeProfiles\Source()); // Simply change the source
// Download the source
$sourcePath = $this->transcodeStorage->downloadToTmp($source);
......
......@@ -84,7 +84,23 @@ class S3Storage implements TranscodeStorageInterface
'Key' => "$this->dir/{$transcode->getGuid()}/{$transcode->getProfile()->getStorageName()}",
'SaveAs' => $sourcePath,
]);
return $sourcePath;
}
/**
* Return a list of files from storage
* @param string $guid
* @return array
*/
public function ls(string $guid): array
{
$awsResult = $this->s3->listObjects([
'Bucket' => 'cinemr',
'Prefix' => "{$this->dir}/{$guid}",
]);
$s3Contents = $awsResult['Contents'];
return array_column($s3Contents, 'Key');
}
}
......@@ -25,4 +25,11 @@ interface TranscodeStorageInterface
* @return string
*/
public function downloadToTmp(Transcode $transcode): string;
/**
* Return a list of files from storage
* @param string $guid
* @return array
*/
public function ls(string $guid): array;
}
......@@ -5,7 +5,10 @@ namespace Spec\Minds\Core\Media\Video;
use Minds\Core\Config;
use Aws\S3\S3Client;
use Minds\Core\Media\Video\Manager;
use Minds\Core\Media\Video\Transcoder;
use Minds\Entities\Video;
use Minds\Core\EntitiesBuilder;
use Minds\Common\Repository\Response;
use Psr\Http\Message\RequestInterface;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
......@@ -14,12 +17,16 @@ class ManagerSpec extends ObjectBehavior
{
private $config;
private $s3;
private $entitiesBuilder;
private $transcoderManager;
public function let(Config $config, S3Client $s3)
public function let(Config $config, S3Client $s3, EntitiesBuilder $entitiesBuilder, Transcoder\Manager $transcoderManager)
{
$this->beConstructedWith($config, $s3);
$this->beConstructedWith($config, $s3, $entitiesBuilder, $transcoderManager);
$this->config = $config;
$this->s3 = $s3;
$this->entitiesBuilder = $entitiesBuilder;
$this->transcoderManager = $transcoderManager;
}
public function it_is_initializable()
......@@ -27,6 +34,55 @@ class ManagerSpec extends ObjectBehavior
$this->shouldHaveType(Manager::class);
}
public function it_should_return_a_video()
{
$video = new Video();
$this->entitiesBuilder->single(123)
->shouldBeCalled()
->willReturn($video);
$this->get(123)
->shouldReturn($video);
}
public function it_should_return_available_sources()
{
$video = new Video();
$video->set('guid', '123');
$this->transcoderManager->getList([
'guid' => '123',
'legacyPolyfill' => true,
])
->shouldBeCalled()
->willReturn(new Response([
(new Transcoder\Transcode())
->setGuid('123')
->setProfile(new Transcoder\TranscodeProfiles\X264_720p())
->setStatus('created'),
(new Transcoder\Transcode())
->setGuid('123')
->setProfile(new Transcoder\TranscodeProfiles\X264_360p())
->setStatus('completed'),
(new Transcoder\Transcode())
->setGuid('123')
->setProfile(new Transcoder\TranscodeProfiles\Webm_360p())
->setStatus('completed'),
]));
$sources = $this->getSources($video);
$sources->shouldHaveCount(2);
$sources[0]->getType()
->shouldBe('video/mp4');
$sources[0]->getSize(360);
$sources[1]->getType()
->shouldBe('video/webm');
$sources[1]->getSize(360);
}
public function it_should_get_a_signed_720p_video_url(RequestInterface $request, \Aws\CommandInterface $cmd)
{
$this->config->get('transcoder')
......
......@@ -12,6 +12,7 @@ use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Media\Video\Transcoder\Delegates\NotificationDelegate;
use Minds\Entities\Video;
use Minds\Entities\User;
use Minds\Common\Repository\Response;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
......@@ -146,4 +147,32 @@ class ManagerSpec extends ObjectBehavior
$this->transcode($transcode);
}
public function it_should_get_legacy_files()
{
$this->repository->getList([
'guid' => '123',
'profileId' => null,
'status' => null,
'legacyPolyfill' => true,
])
->shouldBeCalled()
->willReturn(new Response());
$this->transcodeStorage->ls('123')
->shouldBeCalled()
->willReturn([
'/my-dir/123/360p.mp4',
'/my-dir/123/720p.mp4',
'/my-dir/123/360p.webm',
]);
$transcodes = $this->getList([
'guid' => '123',
'legacyPolyfill' => true,
]);
$transcodes->shouldHaveCount(3);
$transcodes[0]->getProfile()
->getStorageName('360p.mp4');
}
}
......@@ -38,7 +38,10 @@ class FFMpegExecutorSpec extends ObjectBehavior
\FFMpeg\FFProbe\DataMapping\Format $ffprobeFormat,
\FFMpeg\Media\Frame $ffmpegFrame
) {
//$transcode = new Transcode();
$transcode->getGuid()
->shouldBeCalled()
->willReturn('123');
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\Thumbnails());
......@@ -97,6 +100,10 @@ class FFMpegExecutorSpec extends ObjectBehavior
\FFMpeg\FFProbe\DataMapping\Format $ffprobeFormat,
\FFMpeg\Filters\Video\VideoFilters $ffmpegVideoFilters
) {
$transcode->getGuid()
->shouldBeCalled()
->willReturn('123');
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\X264_360p());
......@@ -146,6 +153,10 @@ class FFMpegExecutorSpec extends ObjectBehavior
\FFMpeg\FFProbe\DataMapping\Format $ffprobeFormat,
\FFMpeg\Filters\Video\VideoFilters $ffmpegVideoFilters
) {
$transcode->getGuid()
->shouldBeCalled()
->willReturn('123');
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\X264_360p());
......
Please register or to comment