Commit 465c2658 authored by Mark Harding's avatar Mark Harding

(feat): new transcoder

1 merge request!420Additional stripe steps and ability to add phone number
Pipeline #102372159 passed with stages
in 7 minutes and 4 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);
}
}
......@@ -58,9 +58,7 @@ class upload implements Interfaces\Api
$lease->setGuid($guid)
->setMediaType($mediaType);
$manager
->setFullHD(Session::getLoggedinUser()->isPro())
->complete($lease);
$manager->complete($lease);
break;
}
return Factory::response([]);
......
<?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\Video\Transcoder\TranscodeStates;
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');
$transcodeStates = Di::_()->get('Media\Video\Transcoder\TranscodeStates');
$video = $videoManager->get($pages[0]);
$sources = $videoManager->getSources($video);
$status = $transcodeStates->getStatus($video); // Currently not efficient as no caching
if ($status === TranscodeStates::FAILED && count($sources)) {
$status = TranscodeStates::COMPLETED;
}
Factory::response([
'entity' => $video->export(),
'sources' => Factory::exportable($sources),
'poster' => $video->getIconUrl(),
'transcode_status' => $status,
]);
}
/**
* 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;
}
}
......@@ -4,7 +4,7 @@
*/
namespace Minds\Core\Media\ClientUpload;
use Minds\Core\Media\Services\FFMpeg;
use Minds\Core\Media\Video\Transcoder\Manager as TranscoderManager;
use Minds\Core\GuidBuilder;
use Minds\Core\Entities\Actions\Save;
use Minds\Core\Di\Di;
......@@ -12,35 +12,22 @@ use Minds\Entities\Video;
class Manager
{
/** @var FFMpeg */
private $ffmpeg;
/** @var TranscoderManager */
private $transcoderManager;
/** @var Guid $guid */
private $guid;
/** @var bool */
private $full_hd;
/** @var Save $save */
private $save;
/**
* @param bool $value
* @return Manager
*/
public function setFullHD(bool $value): Manager
{
$this->full_hd = $value;
return $this;
}
public function __construct(
FFMpeg $FFMpeg = null,
TranscoderManager $transcoderManager = null,
GuidBuilder $guid = null,
Save $save = null
) {
$this->ffmpeg = $FFMpeg ?: Di::_()->get('Media\Services\FFMpeg');
$this->transcoderManager = $transcoderManager ?: Di::_()->get('Media\Video\Transcoder\Manager');
$this->guid = $guid ?: new GuidBuilder();
$this->save = $save ?: new Save();
}
......@@ -56,13 +43,13 @@ class Manager
throw new \Exception("$type is not currently supported for client based uploads");
}
$guid = $this->guid->build();
$video = new Video();
$video->set('guid', $this->guid->build());
$this->ffmpeg->setKey($guid);
$preSignedUrl = $this->ffmpeg->getPresignedUrl();
$preSignedUrl = $this->transcoderManager->getClientSideUploadUrl($video);
$lease = new ClientUploadLease();
$lease->setGuid($guid)
$lease->setGuid($video->getGuid())
->setMediaType($type)
->setPresignedUrl($preSignedUrl);
......@@ -84,18 +71,12 @@ class Manager
$video->set('guid', $lease->getGuid());
$video->set('cinemr_guid', $lease->getGuid());
$video->set('access_id', 0); // Hide until published
$video->setFlag('full_hd', $this->full_hd);
// Save the video
$this->save->setEntity($video)->save();
$this->ffmpeg->setKey($lease->getGuid());
// Set the full hd flag
$this->ffmpeg->setFullHD($this->full_hd);
// Start the transcoding process
$this->ffmpeg->transcode();
// Kick off the transcoder
$this->transcoderManager->createTranscodes($video);
return true;
}
......
......@@ -72,10 +72,24 @@ class MediaProvider extends Provider
return new ClientUpload\Manager();
}, ['useFactory' => true]);
// Services
// Services (deprecated)
$this->di->bind('Media\Services\FFMpeg', function ($di) {
return new Services\FFMpeg();
}, ['useFactory' => false]);
// Transcoder
$this->di->bind('Media\Video\Transcoder\Manager', function ($di) {
return new Video\Transcoder\Manager();
}, ['useFactory' => false]);
$this->di->bind('Media\Video\Transcoder\TranscodeStates', function ($di) {
return new Video\Transcoder\TranscodeStates();
}, ['useFactory' => false]);
$this->di->bind('Media\Video\Transcode\TranscodeStorage', function ($di) {
return new Video\Transcoder\TranscodeStorage\S3Storage();
}, ['useFactory' => false]);
}
}
<?php
/**
* Minds FFMpeg.
* Minds FFMpeg. (This now deprecated in favour of Core/Media/Video/Transcoder/Manager)
*/
namespace Minds\Core\Media\Services;
......
......@@ -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,64 @@ 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() != Transcoder\TranscodeStates::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',
$this->config->get('transcoder')['dir'],
$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,
];
}
}
<?php
namespace Minds\Core\Media\Video\Transcoder\Delegates;
use Minds\Core\Di\Di;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Media\Video\Transcoder\TranscodeStates;
use Minds\Entities\Video;
use Minds\Core\Events\EventsDispatcher;
use Minds\Core\EntitiesBuilder;
class NotificationDelegate
{
/** @var TranscodeStates */
private $transcodeStates;
/** @var EventsDispatcher */
private $eventsDispatcher;
/** @var EntitiesBuilder */
private $entitiesBuilder;
public function __construct($transcodeStates = null, $eventsDispatcher = null, $entitiesBuilder = null)
{
$this->transcodeStates = $transcodeStates ?? new TranscodeStates();
$this->eventsDispatcher = $eventsDispatcher ?? Di::_()->get('EventsDispatcher');
$this->entitiesBuilder = $entitiesBuilder ?? Di::_()->get('EntitiesBuilder');
}
/**
* Add a transcode to the queue
* @param Transcode $transcode
* @return void
*/
public function onTranscodeCompleted(Transcode $transcode): void
{
$video = $this->entitiesBuilder->single($transcode->getGuid());
if (!$video || !$video instanceof Video) {
error_log("Video ({$transcode->getGuid()}not found");
return; // TODO: Tell sentry?
}
$status = $this->transcodeStates->getStatus($video);
if ($status === TranscodeStates::COMPLETED) {
// $this->emitCompletedNotification($video);
} elseif ($status === TranscodeStates::FAILED) {
// $this->emitFailedNotification($video);
}
}
/**
* @var Video $video
* @return void
*/
private function emitCompletedNotification(Video $video): void
{
$this->eventsDispatcher->trigger('notification', 'transcoder', [
'to'=> [ $video->getOwnerGuid() ],
'from' => 100000000000000519,
'notification_view' => 'transcode_completed',
'entity' => $video,
]);
}
/**
* @var Video $video
* @return void
*/
private function emitFailedNotification(Video $video): void
{
$this->eventsDispatcher->trigger('notification', 'transcoder', [
'to'=> [ $video->getOwnerGuid() ],
'from' => 100000000000000519,
'notification_view' => 'transcode_failed',
'entity' => $video,
]);
}
}
<?php
namespace Minds\Core\Media\Video\Transcoder\Delegates;
use Minds\Core\Di\Di;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Queue\Interfaces\QueueClient;
class QueueDelegate
{
/** @var QueueClient */
private $queueClient;
public function __construct($queueClient = null)
{
$this->queueClient = $queueClient ?? Di::_()->get('Queue');
}
/**
* Add a transcode to the queue
* @param Transcode $transcode
* @return void
*/
public function onAdd(Transcode $transcode): void
{
$this->queueClient
->setQueue('Transcode')
->send([
'transcode' => serialize($transcode),
]);
}
}
<?php
/**
* Transcoder manager
*/
namespace Minds\Core\Media\Video\Transcoder;
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
{
/** @var TranscodeProfileInterface[] */
const TRANSCODE_PROFILES = [
TranscodeProfiles\Thumbnails::class,
TranscodeProfiles\X264_360p::class,
TranscodeProfiles\X264_720p::class,
TranscodeProfiles\X264_1080p::class,
TranscodeProfiles\Webm_360p::class,
TranscodeProfiles\Webm_720p::class,
TranscodeProfiles\Webm_1080p::class,
];
/** @var int */
const TRANSCODER_TIMEOUT_SECS = 600; // 10 minutes with not progress
/** @var Repository */
private $repository;
/** @var QueueDelegate */
private $queueDelegate;
/** @var TranscodeStorage\TranscodeStorageInterface */
private $transcodeStorage;
/** @var TranscodeExecutors\TranscodeExecutorInterfsce */
private $transcodeExecutor;
/** @var NotificationDelegate */
private $notificationDelegate;
public function __construct($repository = null, $queueDelegate = null, $transcodeStorage = null, $transcodeExecutor = null, $notificationDelegate = null)
{
$this->repository = $repository ?? new Repository();
$this->queueDelegate = $queueDelegate ?? new QueueDelegate();
$this->transcodeStorage = $transcodeStorage ?? new TranscodeStorage\S3Storage();
$this->transcodeExecutor = $transcodeExecutor ?? new TranscodeExecutors\FFMpegExecutor();
$this->notificationDelegate = $notificationDelegate ?? new NotificationDelegate();
}
/**
* Return a list of transcodes
* @return Response
*/
public function getList($opts): ?Response
{
$opts = array_merge([
'guid' => null,
'profileId' => null,
'status' => null,
'legacyPolyfill' => false,
], $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;
}
/**
* Return transcodes for a video by urn
* @param string $urn
* @return Transcodes[]
*/
public function getTranscodesByUrn(string $urn): array
{
return [];
}
/**
* Upload the source file to storage
* Note: This does not register any transcodes. createTranscodes should be called
* @param Video $video
* @param string $path
* @return bool
*/
public function uploadSource(Video $video, string $path): bool
{
// Upload the source file to storage
$source = new Transcode();
$source
->setVideo($video)
->setProfile(new TranscodeProfiles\Source());
return (bool) $this->transcodeStorage->add($source, $path);
}
/**
* This will return a url that can be used by an HTTP client
* to upload the source file
* @param Video $video
* @return string
*/
public function getClientSideUploadUrl(Video $video): string
{
$source = new Transcode();
$source
->setVideo($video)
->setProfile(new TranscodeProfiles\Source());
return $this->transcodeStorage->getClientSideUploadUrl($source);
}
/**
* Create the transcodes from from
* @param Video $video
* @return void
*/
public function createTranscodes(Video $video): void
{
foreach (self::TRANSCODE_PROFILES as $profile) {
try {
$transcode = new Transcode();
$transcode
->setVideo($video)
->setProfile(new $profile)
->setStatus(TranscodeStates::CREATED);
// Add the transcode to database and queue
$this->add($transcode);
} catch (TranscodeProfiles\UnavailableTranscodeProfileException $e) {
continue; // Silently fail and just skip
}
}
}
/**
* Add transcode to the queue
* @param Transcode $transcode
* @return void
*/
public function add(Transcode $transcode): void
{
// Add to repository
$this->repository->add($transcode);
// Notify the background queue
$this->queueDelegate->onAdd($transcode);
}
/**
* Update the transcode entity
* @param Transcode $transcode
* @param array $dirty
* @return bool
*/
public function update(Transcode $transcode, array $dirty = []): bool
{
return $this->repository->update($transcode, $dirty);
}
/**
* Run the transcoder (this is called from Core\QueueRunner\Transcode hook)
* @param Transcode $transcode
* @return void
*/
public function transcode(Transcode $transcode): void
{
// Update the background so everyone knows this is inprogress
$transcode->setStatus('transcoding');
$this->update($transcode, [ 'status' ]);
// Perform the transcode
try {
$ref = $this;
$success = $this->transcodeExecutor->transcode($transcode, function ($progress) use ($ref, $transcode) {
$transcode->setProgress($progress);
$this->update($transcode, [ 'progress' ]);
});
if (!$success) { // This is actually unkown as an exception should have been thrown
throw new TranscodeExecutors\FailedTranscodeException();
}
$transcode->setProgress(100); // If completed should be assumed 100%
$transcode->setStatus('completed');
} catch (TranscodeExecutors\FailedTranscodeException $e) {
$transcode->setStatus('failed');
} finally {
$this->update($transcode, [ 'progress', 'status' ]);
}
$this->notificationDelegate->onTranscodeCompleted($transcode);
}
}
<?php
/**
* Transcoder manager
*/
namespace Minds\Core\Media\Video\Transcoder;
use Minds\Core\Media\Video\Transcode\Delegates\QueueDelegate;
use Minds\Entities\Video;
use Minds\Traits\MagicAttributes;
use Minds\Common\Repository\Response;
use Minds\Common\Urn;
use Minds\Core\Di\Di;
use Minds\Core\Data\Cassandra\Client;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Cassandra\Bigint;
use Cassandra\Timestamp;
class Repository
{
/** @var Client */
private $db;
public function __construct($db = null)
{
$this->db = $db ?? Di::_()->get('Database\Cassandra\Cql');
}
/**
* Return a list of transcodes
* @param array $opts
* @return Response
*/
public function getList(array $opts): Response
{
$opts = array_merge([
'guid' => null,
'profileId' => null,
'status' => null,
], $opts);
$statement = "SELECT
guid,
profile_id,
progress,
status,
last_event_timestamp_ms,
bytes
FROM video_transcodes";
$where = [];
$values = [];
if ($opts['guid']) {
$where[] = "guid = ?";
$values[] = new Bigint($opts['guid']);
}
if ($opts['profileId']) {
$where[] = "profile_id = ?";
$values[] = $opts['profile_id'];
}
if ($opts['status']) {
$where[] = "status = ?";
$values[] = $opts['status'];
}
$statement .= " WHERE " . implode(' AND ', $where);
$prepared = new Custom();
$prepared->query($statement, $values);
try {
$result = $this->db->request($prepared);
} catch (\Exception $e) {
return new Response(); // TODO: make sure error is attached to response
}
$response = new Response;
foreach ($result as $row) {
$response[] = $this->buildTranscodeFromRow($row);
}
return $response;
}
/**
* Return a single transcode
* @param string $urn
* @return Transcode
*/
public function get(string $urn): ?Transcode
{
$urn = Urn::_($urn);
list($guid, $profile) = explode('-', $urn->getNss());
$statement = "SELECT
guid,
profile_id,
progress,
status,
last_event_timestamp_ms,
bytes
FROM video_transcodes
WHERE guid = ?
AND profile = ?";
$values = [
new Bigint($guid),
$profile
];
$prepared = new Custom();
$prepared->query($statement, $values);
try {
$result = $this->db->request($prepared);
} catch (\Exception $e) {
return null;
}
$row = $result[0];
$transcode = $this->buildTranscodeFromRow($row);
return $transcode;
}
/**
* Add a transcode to the database
* @param Transcode $transcode
* @return bool
*/
public function add(Transcode $transcode): bool
{
$statement = "INSERT INTO video_transcodes (guid, profile_id, status) VALUES (?, ?, ?)";
$values = [
new Bigint($transcode->getGuid()),
$transcode->getProfile()->getId(),
$transcode->getStatus(),
];
$prepared = new Custom();
$prepared->query($statement, $values);
try {
$this->db->request($prepared);
} catch (\Exception $e) {
return false;
}
return true;
}
/**
* Update the transcode
* @param Transcode $transcode
* @param array $dirty - list of fields that have changed
* @return bool
*/
public function update(Transcode $transcode, array $dirty = []): bool
{
// Always update lastEventTimestampMs
$transcode->setLastEventTimestampMs(round(microtime(true) * 1000));
$dirty[] = 'lastEventTimestampMs';
$statement = "UPDATE video_transcodes";
$set = [];
foreach ($dirty as $field) {
switch ($field) {
case 'progress':
$set['progress'] = (int) $transcode->getProgress(); // This is a percentage basedf off 100
break;
case 'status':
$set['status'] = (string) $transcode->getStatus();
break;
case 'lastEventTimestampMs':
$set['last_event_timestamp_ms'] = new Timestamp($transcode->getLastEventTimestampMs() / 1000);
break;
case 'lengthSecs':
$set['length_secs'] = (int) $transcode->getLengthSecs();
break;
case 'bytes':
$set['bytes'] = (int) $transcode->getBytes();
break;
}
}
// Convert our $set to statement
$statement .= " SET " . implode(' , ', array_map(function ($field) {
return "$field = ?";
}, array_keys($set)));
// Move to values array
$values = array_values($set);
// Say what we are updating
$statement .= " WHERE guid = ? AND profile_id = ?";
$values[] = new Bigint($transcode->getGuid());
$values[] = (string) $transcode->getProfile()->getId();
// Prepared statement
$prepared = new Custom();
$prepared->query($statement, $values);
try {
$this->db->request($prepared);
} catch (\Exception $e) {
return false;
}
return true;
}
/**
* Delete a transcode
* @param Transcode $transcode
* @return bool
*/
public function delete(Transcode $transcode): bool
{
$statement = "DELETE FROM video_transcodes WHERE guid = ? and profile_id = ?";
$values = [
new Bigint($transcode->getGuid()),
(string) $transcode->getProfile()->getId(),
];
// Prepared statement
$prepared = new Custom();
$prepared->query($statement, $values);
try {
$this->db->request($prepared);
} catch (\Exception $e) {
return false;
}
return true;
}
/**
* Build transcode from an array of data
* @param array $row
* @return Transcode
*/
protected function buildTranscodeFromRow(array $row): Transcode
{
$transcode = new Transcode();
$transcode->setGuid((string) $row['guid'])
->setProfile(TranscodeProfiles\Factory::build((string) $row['profile_id']))
->setProgress($row['progress'])
->setStatus($row['status'])
->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;
}
}
<?php
/**
* Transcode model
*/
namespace Minds\Core\Media\Video\Transcoder;
use Minds\Entities\Video;
use Minds\Traits\MagicAttributes;
/**
* @method Transcode setGuid(string $guid)
* @method string getGuid()
* @method Transcode setVideo(Video $video)
* @method Video getVideo()
* @method Transcode setProgress(int $progress)
* @method int getProgress()
* @method Transcode setStatus(string $status)
* @method string getStatus()
* @method TranscodeProfiles\TranscodeProfileInterface getProfile()
* @method int getLastEventTimestampMs()
* @method Transcode setLastEventTimestampMs(int $lastEventTimestampMs)
* @method Transcode setLengthSecs(int $secs)
* @method int getLengthSecs()
* @method Transcode setBytes(int $bytes)
* @method int getBytes()
*/
class Transcode
{
use MagicAttributes;
/** @var string */
const TRANSCODE_STATES = [
'created',
'transcoding',
'failed',
'completed',
];
/** @var string */
private $guid;
/** @var Video */
private $video;
/** @var TranscodeProfiles\TranscodeProfileInterface */
private $profile;
/** @var int */
private $progress = 0;
/** @var string */
protected $status;
/** @var int */
private $lastEventTimestampMs;
/** @var int */
private $lengthSecs;
/** @var int */
private $bytes;
/**
* @param Video $video
* @return self
*/
public function setVideo(Video $video): self
{
$this->video = $video;
$this->guid = $video->getGuid();
return $this;
}
/**
* Set the profile
* @param TranscodeProfiles\TranscodeProfileInterface $profile
* @throws TranscodeProfiles\UnavailableTranscodeProfileException
* @return self
*/
public function setProfile(TranscodeProfiles\TranscodeProfileInterface $profile): self
{
if ($profile->isProOnly() && $this->video && !$this->video->getOwnerEntity()->isPro()) {
throw new TranscodeProfiles\UnavailableTranscodeProfileException();
}
$this->profile = $profile;
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'guid' => $this->guid,
'profile' => $this->profile->export(),
'progress' => (int) $this->progress,
'completed' => (bool) $this->completed,
'last_event_timestamp_ms' => (int) $this->lastEventTimestampMs,
'length_secs' => (int) $this->lengthSecs,
'bytes' => (int) $this->bytes,
];
}
}
<?php
/**
* Minds FFMpeg.
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeExecutors;
use FFMpeg\FFMpeg as FFMpegClient;
use FFMpeg\FFProbe as FFProbeClient;
use FFMpeg\Media\Video as FFMpegVideo;
use FFMpeg\Filters\Video\ResizeFilter;
use Minds\Core;
use Minds\Core\Config;
use Minds\Entities\Video;
use Minds\Core\Di\Di;
use Minds\Core\Media\TranscodingStatus;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Media\Video\Transcoder\TranscodeStorage\TranscodeStorageInterface;
use Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class FFMpegExecutor implements TranscodeExecutorInterface
{
/** @var Config */
private $config;
/** @var FFMpeg */
private $ffmpeg;
/** @var FFProbe */
private $ffprobe;
/** @var TranscodeStorageInterface */
private $transcodeStorage;
public function __construct(
$config = null,
$ffmpeg = null,
$ffprobe = null,
$transcodeStorage = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->ffmpeg = $ffmpeg ?: FFMpegClient::create([
'ffmpeg.binaries' => '/usr/bin/ffmpeg',
'ffprobe.binaries' => '/usr/bin/ffprobe',
'ffmpeg.threads' => $this->config->get('transcoder')['threads'],
'timeout' => 0,
]);
$this->ffprobe = $ffprobe ?: FFProbeClient::create([
'ffprobe.binaries' => '/usr/bin/ffprobe',
]);
$this->transcodeStorage = $transcodeStorage ?? Di::_()->get('Media\Video\Transcode\TranscodeStorage');
}
/**
* Transcode the video
* @param Transcode $transcode (pass by reference)
* @param callable $progressCallback
* @return bool
*/
public function transcode(Transcode &$transcode, callable $progressCallback): bool
{
// This is the profile that will be used for the transcode
$transcodeProfiler = $transcode->getProfile();
// Prepare the source of this transcode
$source = new Transcode();
$source->setGuid($transcode->getGuid())
->setProfile(new TranscodeProfiles\Source()); // Simply change the source
// Download the source
$sourcePath = $this->transcodeStorage->downloadToTmp($source);
// Open the resource
/** @var \FFMpeg\Media\Video; */
$video = $this->ffmpeg->open($sourcePath);
if (!$video) {
throw new FailedTranscodeException("Source error");
}
$tags = null;
try {
$videostream = $this->ffprobe
->streams($sourcePath)
->videos()
->first();
// get video metadata
$tags = $videostream->get('tags');
} catch (\Exception $e) {
error_log('Error getting videostream information');
}
// Thumbnails are treated differently to other transcodes
if ($transcode->getProfile() instanceof TranscodeProfiles\Thumbnails) {
return $this->transcodeThumbnails($transcode, $sourcePath, $video);
}
// Target height
$width = $transcodeProfiler->getWidth();
$height = $transcodeProfiler->getHeight();
// Logic for rotated videos
$rotated = isset($tags['rotate']) && in_array($tags['rotate'], [270, 90], true);
if ($rotated) {
$ratio = $videostream->get('width') / $videostream->get('height');
// Invert width and height
$width = $height;
$height = round($height * $ratio);
}
// Resize the video
$video->filters()
->resize(
new \FFMpeg\Coordinate\Dimension($width, $height),
$rotated ? ResizeFilter::RESIZEMODE_FIT : ResizeFilter::RESIZEMODE_SCALE_WIDTH
)
->synchronize();
$pfx = $transcodeProfiler->getStorageName();
$path = $sourcePath.'-'.$pfx;
$format = $transcodeProfiler->getFormat();
$formatMap = [
'video/mp4' => (new \FFMpeg\Format\Video\X264())
->setAudioCodec('aac'),
'video/webm' => new \FFMpeg\Format\Video\WebM(),
];
try {
// $this->logger->info("Transcoding: $path ({$transcode->getGuid()})");
// Update our progress
$formatMap[$format]->on('progress', function ($a, $b, $pct) use ($progressCallback) {
// $this->logger->info("$pct% transcoded");
$progressCallback($pct);
});
$formatMap[$format]
->setKiloBitRate($transcodeProfiler->getBitrate())
->setAudioKiloBitrate($transcodeProfiler->getAudioBitrate());
// Run the transcode
$video->save($formatMap[$format], $path);
// Save to storage
$this->transcodeStorage->add($transcode, $path);
// Completed!
// $this->logger->info("Completed: $path ({$transcode->getGuid()})");
$transcode->setStatus('completed');
} catch (\Exception $e) {
// $this->logger->out("Failed {$e->getMessage()}");
$transcode->setStatus('failed');
// TODO: Should we also save the failure reason in the db?
// Throw a new 'failed' exception
throw new FailedTranscodeException($e->getMessage());
} finally {
// Cleanup our path
@unlink($path);
}
// Cleanup our sourcefile
@unlink($sourcePath);
return true;
}
/**
* Thumbnail transcodes are treated differently as they we extract frames
* @param Transcode $transcode
* @return bool
*/
protected function transcodeThumbnails(Transcode &$transcode, string $sourcePath, FFMpegVideo $video): bool
{
try {
// Create a temporary directory for out thumbnails
$thumbnailsDir = $sourcePath . '-thumbnails';
@mkdir($thumbnailsDir, 0600, true);
// Create thumbnails
$length = round((int) $this->ffprobe->format($sourcePath)->get('duration'));
$secs = [0, 1, round($length / 2), $length - 1, $length];
foreach ($secs as $sec) {
$frame = $video->frame(\FFMpeg\Coordinate\TimeCode::fromSeconds($sec));
$pad = str_pad($sec, 5, '0', STR_PAD_LEFT);
$path = $thumbnailsDir.'/'."thumbnail-$pad.png";
$frame->save($path);
// Hack the profile storage name, as there are multiple thumbnails
$transcode->getProfile()->setStorageName("thumbnail-$pad.png");
// Upload to filestore
$this->transcodeStorage->add($transcode, $path);
// Cleanup tmp
@unlink($path);
}
$transcode->setProgress(100);
$transcode->setStatus('completed');
} catch (\Exception $e) {
$transcode->setStatus('failed');
// TODO: Should we also save the failure reason in the db?
// Throw a new 'failed' exception
throw new FailedTranscodeException($e->getMessage());
} finally {
// Cleanup the temporary directory we made
@unlink($thumbnailsDir);
}
return true;
}
}
<?php
namespace Minds\Core\Media\Video\Transcoder\TranscodeExecutors;
class FailedTranscodeException extends \Exception
{
}
<?php
namespace Minds\Core\Media\Video\Transcoder\TranscodeExecutors;
use Minds\Core\Media\Video\Transcoder\Transcode;
interface TranscodeExecutorInterface
{
/**
* @param Transcode $transcode
* @return bool
*/
public function transcode(Transcode &$transcode, callable $progressCallback): bool;
}
<?php
/**
* Abstract class for transcode profiles
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
use Minds\Traits\MagicAttributes;
/**
* @method string getFormat()
* @method int getWidth()
* @method int getHeight()
* @method int getBitrate()
* @method int getAudioBitrate()
* @method bool isProOnly()
* @method string getStorageName()
* @method TranscodeProfileInterface setStorageName(string $storageName)
*/
abstract class AbstractTranscodeProfile implements TranscodeProfileInterface
{
use MagicAttributes;
/** @var string */
protected $format;
/** @var int */
protected $width;
/** @var int */
protected $height;
/** @var int */
protected $bitrate;
/** @var int */
protected $audioBitrate;
/** @var bool */
protected $proOnly = false;
/** @var string */
protected $storageName;
/**
* Returns the ID of the transcode (this will usually be the classname)
* @return string
*/
public function getId(): string
{
$path = explode('\\', get_called_class());
return array_pop($path);
}
/**
* Export the profile
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'id' => $this->getId(),
'format' => $this->format,
'width' => (int) $this->width,
'height' => (int) $this->height,
'bitrate' => (int) $this->bitrate,
'audio_bitrate' => (int) $this->audioBitrarte,
];
}
}
<?php
/**
* Source. This is the source file.
* It will not be transcoded but will be stored on the filestore
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class Factory
{
/**
* Build a TranscodeProfileInterface from ID
* @param string $profileId
* @return TranscodeProfile
* @throws TranscodeProfileNotFoundException
*/
public static function build(string $profileId): TranscodeProfileInterface
{
$class = "Minds\\Core\\Media\\Video\\Transcoder\\TranscodeProfiles\\$profileId";
if (class_exists($class)) {
return new $class;
}
throw new TranscodeProfileNotFoundException("$profileId does not have a valid profile instance");
}
}
<?php
/**
* Source. This is the source file.
* It will not be transcoded but will be stored on the filestore
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class Source extends AbstractTranscodeProfile
{
/** @var string */
protected $format = 'video/*';
/** @var int */
protected $width = 0;
/** @var int */
protected $height = 0;
/** @var string */
protected $storageName = 'source';
}
<?php
/**
* Thumbnails
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class Thumbnails extends AbstractTranscodeProfile
{
/** @var string */
protected $format = 'image/png';
/** @var int */
protected $width = 1920;
/** @var int */
protected $height = 1080;
}
<?php
/**
* Transcode Profile Interface
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
interface TranscodeProfileInterface
{
}
<?php
/**
* Unavailable Transcode Profile Exceptioon
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class TranscodeProfileNotFoundException extends \Exception
{
}
<?php
/**
* Unavailable Transcode Profile Exceptioon
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class UnavailableTranscodeProfileException extends \Exception
{
/** @var string */
protected $message = 'This transcode profile is not available';
}
<?php
/**
* 1080p Webm (pro only)
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class Webm_1080p extends AbstractTranscodeProfile
{
/** @var string */
protected $format = 'video/webm';
/** @var int */
protected $width = 1920;
/** @var int */
protected $height = 1080;
/** @var int */
protected $bitrate = 2000;
/** @var int */
protected $audioBitrate = 128;
/** @var bool */
protected $proOnly = true;
/** @var string */
protected $storageName = '1080.webm';
}
<?php
/**
* 360p Webm
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class Webm_360p extends AbstractTranscodeProfile
{
/** @var string */
protected $format = 'video/webm';
/** @var int */
protected $width = 640;
/** @var int */
protected $height = 360;
/** @var int */
protected $bitrate = 500;
/** @var int */
protected $audioBitrate = 80;
/** @var bool */
protected $proOnly = false;
/** @var string */
protected $storageName = '360.webm';
}
<?php
/**
* 720p Webm
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class Webm_720p extends AbstractTranscodeProfile
{
/** @var string */
protected $format = 'video/webm';
/** @var int */
protected $width = 1280;
/** @var int */
protected $height = 720;
/** @var int */
protected $bitrate = 1000;
/** @var int */
protected $audioBitrate = 128;
/** @var bool */
protected $proOnly = false;
/** @var string */
protected $storageName = '720.webm';
}
<?php
/**
* 1080p MP4 (pro only)
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class X264_1080p extends AbstractTranscodeProfile
{
/** @var string */
protected $format = 'video/mp4';
/** @var int */
protected $width = 1920;
/** @var int */
protected $height = 1080;
/** @var int */
protected $bitrate = 2000;
/** @var int */
protected $audioBitrate = 128;
/** @var bool */
protected $proOnly = true;
/** @var string */
protected $storageName = '1080.mp4';
}
<?php
/**
* 360p MP4
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class X264_360p extends AbstractTranscodeProfile
{
/** @var string */
protected $format = 'video/mp4';
/** @var int */
protected $width = 640;
/** @var int */
protected $height = 360;
/** @var int */
protected $bitrate = 500;
/** @var int */
protected $audioBitrate = 80;
/** @var bool */
protected $proOnly = false;
/** @var string */
protected $storageName = '360.mp4';
}
<?php
/**
* 720p MP4
*/
namespace Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
class X264_720p extends AbstractTranscodeProfile
{
/** @var string */
protected $format = 'video/mp4';
/** @var int */
protected $width = 1280;
/** @var int */
protected $height = 720;
/** @var int */
protected $bitrate = 1000;
/** @var int */
protected $audioBitrate = 128;
/** @var bool */
protected $proOnly = false;
/** @var string */
protected $storageName = '720.mp4';
}
<?php
namespace Minds\Core\Media\Video\Transcoder;
use Minds\Entities\Video;
class TranscodeStates
{
/** @var string */
public const CREATED = 'created';
/** @var string */
public const TRANSCODING = 'transcoding';
/** @var string */
public const FAILED = 'failed';
/** @var string */
public const COMPLETED = 'completed';
/** @var Repository */
private $repository;
public function __construct($repository = null)
{
// NOTE: We are using repository as this is called via
// Delegates\NotificationDelegate and it causes an infinite loop
// with the manager
$this->repository = $repository ?? new Repository();
}
/**
* Return the overral transcoding status
* MH: I don't love this function at all!
* @param Video $video
* @return string
*/
public function getStatus(Video $video): string
{
$transcodes = $this->repository->getList([
'guid' => $video->getGuid(),
]);
$total = 0;
$created = 0;
$failures = 0;
$completed = 0;
foreach ($transcodes as $transcode) {
if ($transcode instanceof TranscodeProfiles\Thumbnails) {
continue; // We skip thumbnails as these are likely to succeed
}
++$total;
switch ($transcode->getStatus()) {
case TranscodeStates::TRANSCODING:
if ($transcode->getLastEventTimestampMs() >= (time() - Manager::TRANSCODER_TIMEOUT_SECS) * 1000) {
// Still transcoding
return TranscodeStates::TRANSCODING;
} else {
++$failures;
}
break;
case TranscodeStates::CREATED:
// If not started to transcode then we are in a created state
++$created;
break;
case TranscodeStates::FAILED:
++$failures;
// We should allow failures for some transcodes
break;
case TranscodeStates::COMPLETED:
++$completed;
break;
}
}
if ($created > ($completed + $failures)) {
return TranscodeStates::CREATED;
}
if ($total < ($completed + $failures)) {
return TranscodeStates::CREATED;
}
// If we have more completions then failures the declare completed
if ($failures < $completed) {
return TranscodeStates::COMPLETED;
}
return TranscodeStates::FAILED; // Our default state is failed?
}
}
<?php
namespace Minds\Core\Media\Video\Transcoder\TranscodeStorage;
use Aws\S3\S3Client;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Media\Video\Transcoder\Transcode;
class S3Storage implements TranscodeStorageInterface
{
/** @var string */
private $dir = 'cinemr_data';
/** @var Config */
private $config;
/** @var S3Client */
private $s3;
public function __construct($config = null, $s3 = null)
{
$this->config = $config ?? Di::_()->get('Config');
$this->dir = $this->config->get('transcoder')['dir'];
$awsConfig = $this->config->get('aws');
$opts = [
'region' => $awsConfig['region'],
];
if (!isset($awsConfig['useRoles']) || !$awsConfig['useRoles']) {
$opts['credentials'] = [
'key' => $awsConfig['key'],
'secret' => $awsConfig['secret'],
];
}
$this->s3 = $s3 ?: new S3Client(array_merge(['version' => '2006-03-01'], $opts));
}
/**
* Add a transcode to storage
* @param Transcode $transcode
* @param string $path
* @return bool
*/
public function add(Transcode $transcode, string $path): bool
{
return (bool) $this->s3->putObject([
'ACL' => 'public-read',
'Bucket' => 'cinemr',
'Key' => "$this->dir/{$transcode->getGuid()}/{$transcode->getProfile()->getStorageName()}",
'Body' => fopen($path, 'r'),
]);
}
/**
* This will return a url that can be used by an HTTP client
* to upload the source file
* @param Transcode $transcode
* @return string
*/
public function getClientSideUploadUrl(Transcode $transcode): string
{
$cmd = $this->s3->getCommand('PutObject', [
'Bucket' => 'cinemr',
'Key' => "$this->dir/{$transcode->getGuid()}/{$transcode->getProfile()->getStorageName()}",
]);
return (string) $this->s3->createPresignedRequest($cmd, '+20 minutes')->getUri();
}
/**
* @param Transcode $transcode
* @return string
*/
public function downloadToTmp(Transcode $transcode): string
{
// Create a temporary file where our source file will go
$sourcePath = tempnam(sys_get_temp_dir(), "{$transcode->getGuid()}-{$transcode->getProfile()->getStorageName()}");
// Grab from S3
$this->s3->getObject([
'Bucket' => 'cinemr',
'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');
}
}
<?php
namespace Minds\Core\Media\Video\Transcoder\TranscodeStorage;
use Minds\Core\Media\Video\Transcoder\Transcode;
interface TranscodeStorageInterface
{
/**
* @param Transcode $transcode
* @param string $path
* @return bool
*/
public function add(Transcode $transcode, string $path): bool;
/**
* This will return a url that can be used by an HTTP client
* to upload the source file
* @param Transcode $transcode
* @return string
*/
public function getClientSideUploadUrl(Transcode $transcode): string;
/**
* @param Transcode $transcode
* @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;
}
<?php
/**
* Transcode model
*/
namespace Minds\Core\Media\Video\Transcoder;
use Minds\Traits\MagicAttributes;
class Transcodes
{
use MagicAttributes;
/** @var string */
private $guid;
/** @var Transcode[] */
private $transcodes;
public function export($extras = []): array
{
return [
'guid' => $this->guid,
'transcodes' => $this->transcodes ? array_map(function ($transcode) {
return $transcode->export();
}, $this->transcodes) : null
];
}
}
......@@ -1533,3 +1533,14 @@ CREATE MATERIALIZED VIEW minds.withdrawals_by_status AS
WHERE status IS NOT NULL AND user_guid IS NOT NULL AND timestamp IS NOT NULL AND tx IS NOT NULL
PRIMARY KEY (status, timestamp, user_guid, tx)
WITH CLUSTERING ORDER BY (timestamp ASC, user_guid ASC, tx ASC);
CREATE TABLE minds.video_transcodes (
guid bigint,
profile_id text,
progress int,
status text,
last_event_timestamp_ms timestamp,
length_secs int,
bytes int,
PRIMARY KEY (guid, profile_id)
);
\ No newline at end of file
......@@ -2,6 +2,7 @@
namespace Minds\Core\Queue\Runners;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Core\Queue\Interfaces;
class Transcode implements Interfaces\QueueRunner
......@@ -14,11 +15,12 @@ class Transcode implements Interfaces\QueueRunner
$client->setQueue("Transcode")
->receive(function ($data) {
$d = $data->getData();
$transcode = unserialize($d['transcode']);
echo "Received a transcode request \n";
$transcoder = new Core\Media\Services\FFMpeg();
$transcoder->setKey($d['key']);
$transcoder->setFullHD($d['full_hd']);
$transcoder->onQueue();
$transcoderManager = Di::_()->get('Media\Video\Transcoder\Manager');
$transcoderManager->transcode($transcode);
}, [ 'max_messages' => 1 ]);
}
}
......@@ -51,14 +51,15 @@ class Video extends MindsObject
public function upload($filepath)
{
// TODO: Confirm why this is still here
$this->generateGuid();
$transcoder = ServiceFactory::build('FFMpeg');
$transcoder->setKey($this->getGuid())
->setFullHD($this->getFlag('full_hd'))
->saveToFilestore($filepath)
->transcode();
// Upload the source and start the transcoder pipeline
$transcoderManager = Di::_()->get('Media\Video\Transcoder\Manager');
$transcoderManager->uploadSource($this, $filepath)
->createTranscodes($this);
// Legacy support
$this->cinemr_guid = $this->getGuid();
}
......
......@@ -4,23 +4,24 @@ namespace Spec\Minds\Core\Media\ClientUpload;
use Minds\Core\Media\ClientUpload\Manager;
use Minds\Core\Media\ClientUpload\ClientUploadLease;
use Minds\Core\Media\Services\FFMpeg;
use Minds\Core\Media\Video\Transcoder;
use Minds\Core\GuidBuilder;
use Minds\Core\Entities\Actions\Save;
use Minds\Entities\Video;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ManagerSpec extends ObjectBehavior
{
private $ffmpeg;
private $transcoderManager;
private $guid;
private $save;
public function let(FFMpeg $FFMpeg, GuidBuilder $guid, Save $save)
public function let(Transcoder\Manager $transcoderManager, GuidBuilder $guid, Save $save)
{
$this->beConstructedWith($FFMpeg, $guid, $save);
$this->ffmpeg = $FFMpeg;
$this->beConstructedWith($transcoderManager, $guid, $save);
$this->transcoderManager = $transcoderManager;
$this->guid = $guid;
$this->save = $save;
}
......@@ -35,10 +36,8 @@ class ManagerSpec extends ObjectBehavior
$this->guid->build()
->willReturn(123);
$this->ffmpeg->setKey(123)
->shouldBeCalled();
$this->ffmpeg->getPresignedUrl()
$this->transcoderManager->getClientSideUploadUrl(Argument::type(Video::class))
->shouldBeCalled()
->willReturn('s3-url-here');
$lease = $this->prepare('video');
......@@ -69,17 +68,10 @@ class ManagerSpec extends ObjectBehavior
$this->save->save()
->shouldBeCalled();
$this->ffmpeg->setKey(456)
->shouldBeCalled();
$this->ffmpeg->setFullHD(false)
->shouldBeCalled();
$this->ffmpeg->transcode()
$this->transcoderManager->createTranscodes(Argument::type(Video::class))
->shouldBeCalled();
$this->setFullHD(false)
->complete($lease)
$this->complete($lease)
->shouldReturn(true);
}
}
......@@ -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')
......
<?php
namespace Spec\Minds\Core\Media\Video\Transcoder\Delegates;
use Minds\Core\Media\Video\Transcoder\Delegates\NotificationDelegate;
use Minds\Core\Media\Video\Transcoder\TranscodeStates;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Events\EventsDispatcher;
use Minds\Core\EntitiesBuilder;
use Minds\Entities\Video;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class NotificationDelegateSpec extends ObjectBehavior
{
private $transcodeStates;
private $eventsDispatcher;
private $entitiesBuilder;
public function let(TranscodeStates $transcodeStates, EventsDispatcher $eventsDispatcher, EntitiesBuilder $entitiesBuilder)
{
$this->beConstructedWith($transcodeStates, $eventsDispatcher, $entitiesBuilder);
$this->transcodeStates = $transcodeStates;
$this->eventsDispatcher = $eventsDispatcher;
$this->entitiesBuilder = $entitiesBuilder;
}
private function mockFetchVideo()
{
$this->entitiesBuilder->single('123')
->shouldBeCalled()
->willReturn(
(new Video)
->set('guid', '123')
->set('owner_guid', '456')
);
}
public function it_is_initializable()
{
$this->shouldHaveType(NotificationDelegate::class);
}
// public function it_should_send_notification_of_completed()
// {
// $transcode = new Transcode();
// $transcode->setGuid('123');
// $this->mockFetchVideo();
// $this->transcodeStates->getStatus(Argument::that(function ($video) {
// return $video->getGuid() === '123';
// }))
// ->shouldBeCalled()
// ->willReturn('completed');
// $this->eventsDispatcher->trigger('notification', 'transcoder', Argument::type('array'))
// ->shouldBeCalled();
// $this->onTranscodeCompleted($transcode);
// }
// public function it_should_send_notification_of_failed()
// {
// $transcode = new Transcode();
// $transcode->setGuid('123');
// $this->mockFetchVideo();
// $this->transcodeStates->getStatus(Argument::that(function ($video) {
// return $video->getGuid() === '123';
// }))
// ->shouldBeCalled()
// ->willReturn('failed');
// $this->eventsDispatcher->trigger('notification', 'transcoder', Argument::type('array'))
// ->shouldBeCalled();
// $this->onTranscodeCompleted($transcode);
// }
public function it_should_do_nothing()
{
$transcode = new Transcode();
$transcode->setGuid('123');
$this->mockFetchVideo();
$this->transcodeStates->getStatus(Argument::that(function ($video) {
return $video->getGuid() === '123';
}))
->shouldBeCalled()
->willReturn('transcoding');
$this->eventsDispatcher->trigger('notification', 'transcoder', Argument::type('array'))
->shouldNotBeCalled();
$this->onTranscodeCompleted($transcode);
}
}
<?php
namespace Spec\Minds\Core\Media\Video\Transcoder\Delegates;
use Minds\Core\Media\Video\Transcoder\Delegates\QueueDelegate;
use Minds\Core\Queue\Interfaces\QueueClient;
use Minds\Core\Media\Video\Transcoder\Transcode;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class QueueDelegateSpec extends ObjectBehavior
{
private $queueClient;
public function let(QueueClient $queueClient)
{
$this->beConstructedWith($queueClient);
$this->queueClient = $queueClient;
}
public function it_is_initializable()
{
$this->shouldHaveType(QueueDelegate::class);
}
public function it_should_add_to_queue()
{
$transcode = new Transcode();
$this->queueClient->setQueue('Transcode')
->shouldBeCalled()
->willReturn($this->queueClient);
$this->queueClient->send(Argument::that(function ($message) use ($transcode) {
return unserialize($message['transcode']) == $transcode;
}))
->shouldBeCalled();
$this->onAdd($transcode);
}
}
<?php
namespace Spec\Minds\Core\Media\Video\Transcoder;
use Minds\Core\Media\Video\Transcoder\Manager;
use Minds\Core\Media\Video\Transcoder\Repository;
use Minds\Core\Media\Video\Transcoder\Delegates\QueueDelegate;
use Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
use Minds\Core\Media\Video\Transcoder\TranscodeStorage\TranscodeStorageInterface;
use Minds\Core\Media\Video\Transcoder\TranscodeExecutors\TranscodeExecutorInterface;
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;
class ManagerSpec extends ObjectBehavior
{
private $repository;
private $queueDelegate;
private $transcodeStorage;
private $transcodeExecutor;
private $notificationDelegate;
public function let(Repository $repository, QueueDelegate $queueDelegate, TranscodeStorageInterface $transcodeStorage, TranscodeExecutorInterface $transcodeExecutor, NotificationDelegate $notificationDelegate)
{
$this->beConstructedWith($repository, $queueDelegate, $transcodeStorage, $transcodeExecutor, $notificationDelegate);
$this->repository = $repository;
$this->queueDelegate = $queueDelegate;
$this->transcodeStorage = $transcodeStorage;
$this->transcodeExecutor = $transcodeExecutor;
$this->notificationDelegate = $notificationDelegate;
}
public function it_is_initializable()
{
$this->shouldHaveType(Manager::class);
}
public function it_should_upload_source_file_to_transcoder_storage()
{
$video = new Video();
$video->guid = 123;
$this->transcodeStorage->add(Argument::that(function ($transcode) use ($video) {
return $transcode->getProfile() instanceof TranscodeProfiles\Source
&& $transcode->getVideo()->getGuid() === $video->getGuid();
}), '/tmp/my-fake-video')
->shouldBeCalled()
->willReturn(true);
$this->uploadSource($video, '/tmp/my-fake-video')
->shouldReturn(true);
}
public function it_should_return_a_signed_url_for_client_upload()
{
$this->transcodeStorage->getClientSideUploadUrl(Argument::type(Transcode::class))
->shouldBeCalled()
->willReturn('signed-url-here');
$this->getClientSideUploadUrl(new Video())
->shouldBe('signed-url-here');
}
public function it_should_create_transcodes_from_video()
{
$video = new Video();
$video->guid = 123;
$user = new User();
$user->pro = 0;
$video->set('owner', $user);
foreach ([
TranscodeProfiles\Thumbnails::class,
TranscodeProfiles\X264_360p::class,
TranscodeProfiles\X264_720p::class,
TranscodeProfiles\Webm_360p::class,
TranscodeProfiles\Webm_720p::class,
] as $i => $profile) {
// Should be added to repo
$this->repository->add(Argument::that(function ($transcode) use ($video, $profile) {
return $transcode->getProfile() instanceof $profile
&& $transcode->getGuid() === $video->getGuid();
}))
->shouldBeCalled();
// And queue
$this->queueDelegate->onAdd(Argument::that(function ($transcode) use ($video, $profile) {
return $transcode->getProfile() instanceof $profile
&& $transcode->getGuid() === $video->getGuid();
}))
->shouldBeCalled();
}
// Should not add 1080p
$this->repository->add(Argument::that(function ($transcode) use ($video, $profile) {
return $transcode->getProfile() instanceof TranscodeProfiles\X264_1080p
&& $transcode->getGuid() === $video->getGuid();
}))
->shouldNotBeCalled();
$this->createTranscodes($video);
}
public function it_should_add_transcode()
{
$transcode = new Transcode();
$this->repository->add($transcode)
->shouldBeCalled();
$this->queueDelegate->onAdd($transcode)
->shouldBeCalled();
$this->add($transcode);
}
public function it_should_update()
{
$transcode = new Transcode();
$this->repository->update($transcode, [ 'myDirtyField' ])
->shouldBeCalled();
$this->update($transcode, [ 'myDirtyField' ]);
}
public function it_should_execute_the_transcode()
{
$transcode = new Transcode();
$transcode->setGuid('123');
$this->repository->update(Argument::that(function ($transcode) {
return $transcode->getStatus() === 'transcoding';
}), [ 'status' ])
->shouldBeCalled();
$this->transcodeExecutor->transcode($transcode, Argument::any())
->shouldBeCalled()
->willReturn(true);
$this->repository->update(Argument::that(function ($transcode) {
return $transcode->getStatus() === 'completed';
}), [ 'progress', 'status' ])
->shouldBeCalled();
// Check for future transcodes is called
$this->notificationDelegate->onTranscodeCompleted($transcode)
->shouldBeCalled();
$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/360.mp4',
'/my-dir/123/720.mp4',
'/my-dir/123/360.webm',
]);
$transcodes = $this->getList([
'guid' => '123',
'legacyPolyfill' => true,
]);
$transcodes->shouldHaveCount(3);
$transcodes[0]->getProfile()
->getStorageName('360.mp4');
}
}
<?php
namespace Spec\Minds\Core\Media\Video\Transcoder;
use Minds\Core\Media\Video\Transcoder\Repository;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
use Minds\Core\Data\Cassandra\Client;
use Spec\Minds\Mocks\Cassandra\Rows;
use Cassandra\Bigint;
use Cassandra\Timestamp;
use Cassandra\Varint;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class RepositorySpec extends ObjectBehavior
{
private $db;
public function let(Client $db)
{
$this->beConstructedWith($db);
$this->db = $db;
}
public function it_is_initializable()
{
$this->shouldHaveType(Repository::class);
}
public function it_should_add(Transcode $transcode)
{
$transcode->getGuid()
->shouldBeCalled()
->willReturn("123");
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\X264_360p());
$transcode->getStatus()
->shouldBeCalled()
->willReturn('created');
$this->db->request(Argument::that(function ($prepared) {
return true;
}))
->shouldBeCalled();
$this->add($transcode)
->shouldReturn(true);
}
public function it_should_get_single()
{
$this->db->request(Argument::that(function ($prepared) {
return true;
}))
->shouldBeCalled()
->willReturn(new Rows([
[
'guid' => new Bigint(123),
'profile_id' => 'X264_360p',
'last_event_timestamp_ms' => new Timestamp(microtime(true)),
'progress' => new Varint(0),
'status' => null,
'length_secs' => new Varint(0),
'bytes' => new Varint(0),
]
], null));
$transcode = $this->get("urn:transcode:123-X264_360p");
$transcode->getGuid()
->shouldBe("123");
}
public function it_should_get_list()
{
$this->db->request(Argument::that(function ($prepared) {
return true;
}))
->shouldBeCalled()
->willReturn(new Rows([
[
'guid' => new Bigint(123),
'profile_id' => 'X264_360p',
'last_event_timestamp_ms' => new Timestamp(microtime(true)),
'progress' => new Varint(0),
'status' => null,
'length_secs' => new Varint(0),
'bytes' => new Varint(0),
],
[
'guid' => new Bigint(456),
'profile_id' => 'X264_720p',
'last_event_timestamp_ms' => new Timestamp(microtime(true)),
'progress' => new Varint(0),
'status' => null,
'length_secs' => new Varint(0),
'bytes' => new Varint(0),
]
], null));
$rows = $this->getList([]);
$X264_360Transcode = $rows[0];
$X264_360Transcode->getGuid()
->shouldBe("123");
$X264_720Transcode = $rows[1];
$X264_720Transcode->getGuid()
->shouldBe("456");
}
public function it_should_update(Transcode $transcode)
{
$transcode->getGuid()
->shouldBeCalled()
->willReturn("123");
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\X264_360p());
$transcode->setLastEventTimestampMs(Argument::approximate(round(microtime(true) * 1000), -4))
->shouldBeCalled();
$transcode->getLastEventTimestampMs()
->shouldBeCalled()
->willReturn(round(microtime(true) * 1000));
$this->db->request(Argument::that(function ($prepared) {
return true;
}))
->shouldBeCalled();
$this->update($transcode, [])
->shouldReturn(true);
}
public function it_should_delete(Transcode $transcode)
{
$transcode->getGuid()
->shouldBeCalled()
->willReturn("123");
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\X264_360p());
$this->db->request(Argument::that(function ($prepared) {
return true;
}))
->shouldBeCalled();
$this->delete($transcode)
->shouldReturn(true);
}
}
<?php
namespace Spec\Minds\Core\Media\Video\Transcoder\TranscodeExecutors;
use Minds\Core\Media\Video\Transcoder\TranscodeExecutors\FFMpegExecutor;
use Minds\Core\Media\Video\Transcoder\TranscodeStorage\TranscodeStorageInterface;
use Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Media\Video\Transcoder\TranscodeExecutors\FailedTranscodeException;
use FFMpeg\FFMpeg as FFMpegClient;
use FFMpeg\FFProbe as FFProbeClient;
use FFMpeg\Filters\Video\ResizeFilter;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class FFMpegExecutorSpec extends ObjectBehavior
{
private $ffmpeg;
private $ffprobe;
private $transcodeStorage;
public function let(FFMpegClient $ffmpeg, FFProbeClient $ffprobe, TranscodeStorageInterface $transcodeStorage)
{
$this->beConstructedWith(null, $ffmpeg, $ffprobe, $transcodeStorage);
$this->ffmpeg = $ffmpeg;
$this->ffprobe = $ffprobe;
$this->transcodeStorage = $transcodeStorage;
}
public function it_is_initializable()
{
$this->shouldHaveType(FFMpegExecutor::class);
}
public function it_should_transcode_thumbnails(
Transcode $transcode,
\FFMpeg\Media\Video $ffmpegVideo,
\FFMpeg\FFProbe\DataMapping\Format $ffprobeFormat,
\FFMpeg\Media\Frame $ffmpegFrame
) {
$transcode->getGuid()
->shouldBeCalled()
->willReturn('123');
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\Thumbnails());
$this->transcodeStorage->downloadToTmp(Argument::type(Transcode::class))
->willReturn('/tmp/fake-path-for-source');
$this->ffmpeg->open('/tmp/fake-path-for-source')
->shouldBeCalled()
->willReturn($ffmpegVideo);
$this->ffprobe->streams('/tmp/fake-path-for-source')
->shouldBeCalled()
->willReturn($this->ffprobe);
$this->ffprobe->format('/tmp/fake-path-for-source')
->shouldBeCalled()
->willReturn($ffprobeFormat);
$ffprobeFormat->get('duration')
->willReturn(120);
$ffmpegVideo->frame(Argument::any())
->shouldBeCalled()
->willReturn($ffmpegFrame);
$ffmpegFrame->save(Argument::any())
->shouldBeCalled();
// These are all the thumbnails thast should have been called
$this->transcodeStorage->add($transcode, '/tmp/fake-path-for-source-thumbnails/thumbnail-00000.png')
->shouldBeCalled();
$this->transcodeStorage->add($transcode, '/tmp/fake-path-for-source-thumbnails/thumbnail-00001.png')
->shouldBeCalled();
$this->transcodeStorage->add($transcode, '/tmp/fake-path-for-source-thumbnails/thumbnail-00060.png')
->shouldBeCalled();
$this->transcodeStorage->add($transcode, '/tmp/fake-path-for-source-thumbnails/thumbnail-00119.png')
->shouldBeCalled();
$this->transcodeStorage->add($transcode, '/tmp/fake-path-for-source-thumbnails/thumbnail-00120.png')
->shouldBeCalled();
$transcode->setProgress(100)
->shouldBeCalled();
$transcode->setStatus('completed')
->shouldBeCalled();
$wrapped = $transcode->getWrappedObject();
$this->getWrappedObject()->transcode($wrapped, function ($progress) {
});
}
public function it_should_transcode_video(
Transcode $transcode,
\FFMpeg\Media\Video $ffmpegVideo,
\FFMpeg\FFProbe\DataMapping\Format $ffprobeFormat,
\FFMpeg\Filters\Video\VideoFilters $ffmpegVideoFilters
) {
$transcode->getGuid()
->shouldBeCalled()
->willReturn('123');
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\X264_360p());
$this->transcodeStorage->downloadToTmp(Argument::type(Transcode::class))
->willReturn('/tmp/fake-path-for-source');
$this->ffmpeg->open('/tmp/fake-path-for-source')
->shouldBeCalled()
->willReturn($ffmpegVideo);
$this->ffprobe->streams('/tmp/fake-path-for-source')
->shouldBeCalled()
->willReturn($this->ffprobe);
$ffmpegVideo->filters()
->shouldBeCalled()
->willReturn($ffmpegVideoFilters);
$ffmpegVideoFilters->resize(Argument::any(), ResizeFilter::RESIZEMODE_SCALE_WIDTH)
->shouldBeCalled()
->willReturn($ffmpegVideoFilters);
$ffmpegVideoFilters->synchronize()
->shouldBeCalled();
$ffmpegVideo->save(Argument::that(function ($format) {
return $format->getKiloBitRate() === 500
&& $format->getAudioKiloBitrate() === 80;
}), '/tmp/fake-path-for-source-360.mp4')
->shouldBeCalled();
$this->transcodeStorage->add($transcode, '/tmp/fake-path-for-source-360.mp4')
->shouldBeCalled();
$transcode->setStatus('completed')
->shouldBeCalled();
$wrapped = $transcode->getWrappedObject();
$this->getWrappedObject()->transcode($wrapped, function ($progress) {
});
}
public function it_should_transcode_video_but_register_failure(
Transcode $transcode,
\FFMpeg\Media\Video $ffmpegVideo,
\FFMpeg\FFProbe\DataMapping\Format $ffprobeFormat,
\FFMpeg\Filters\Video\VideoFilters $ffmpegVideoFilters
) {
$transcode->getGuid()
->shouldBeCalled()
->willReturn('123');
$transcode->getProfile()
->shouldBeCalled()
->willReturn(new TranscodeProfiles\X264_360p());
$this->transcodeStorage->downloadToTmp(Argument::type(Transcode::class))
->willReturn('/tmp/fake-path-for-source');
$this->ffmpeg->open('/tmp/fake-path-for-source')
->shouldBeCalled()
->willReturn($ffmpegVideo);
$this->ffprobe->streams('/tmp/fake-path-for-source')
->shouldBeCalled()
->willReturn($this->ffprobe);
$ffmpegVideo->filters()
->shouldBeCalled()
->willReturn($ffmpegVideoFilters);
$ffmpegVideoFilters->resize(Argument::any(), ResizeFilter::RESIZEMODE_SCALE_WIDTH)
->shouldBeCalled()
->willReturn($ffmpegVideoFilters);
$ffmpegVideoFilters->synchronize()
->shouldBeCalled();
$ffmpegVideo->save(Argument::that(function ($format) {
return $format->getKiloBitRate() === 500
&& $format->getAudioKiloBitrate() === 80;
}), '/tmp/fake-path-for-source-360.mp4')
->shouldBeCalled()
->willThrow(new \FFMpeg\Exception\RuntimeException());
$this->transcodeStorage->add($transcode, '/tmp/fake-path-for-source-360.mp4')
->shouldNotBeCalled();
$transcode->setStatus('failed')
->shouldBeCalled();
$wrapped = $transcode->getWrappedObject();
try {
$this->getWrappedObject()->transcode($wrapped, function ($progress) {
});
throw new \Exception("An exception should have been thrown doe failed transcode");
} catch (FailedTranscodeException $e) {
// We throw a new exception above if the one we are expecting isn't called
}
}
}
<?php
namespace Spec\Minds\Core\Media\Video\Transcoder;
use Minds\Core\Media\Video\Transcoder\TranscodeStates;
use Minds\Core\Media\Video\Transcoder\Repository;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
use Minds\Entities\Video;
use Minds\Common\Repository\Response;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class TranscodeStatesSpec extends ObjectBehavior
{
private $repository;
public function let(Repository $repository)
{
$this->beConstructedWith($repository);
$this->repository = $repository;
}
public function it_is_initializable()
{
$this->shouldHaveType(TranscodeStates::class);
}
public function it_should_return_transcoding_status()
{
$video = new Video();
$video->set('guid', '123');
$this->repository->getList([ 'guid' => '123' ])
->shouldBeCalled()
->willReturn(new Response([
(new Transcode())
->setProfile(new TranscodeProfiles\X264_360p())
->setStatus('transcoding')
->setLastEventTimestampMs(round(microtime(true) * 1000)),
(new Transcode())
->setProfile(new TranscodeProfiles\X264_720p())
->setStatus('completed')
->setLastEventTimestampMs(round(microtime(true) * 1000))
]));
$this->getStatus($video)
->shouldReturn('transcoding');
}
public function it_should_declare_a_timeout()
{
$video = new Video();
$video->set('guid', '123');
$this->repository->getList([ 'guid' => '123' ])
->shouldBeCalled()
->willReturn(new Response([
(new Transcode())
->setProfile(new TranscodeProfiles\X264_360p())
->setStatus('transcoding')
->setLastEventTimestampMs(round(microtime(true) * 1000) - 700000),
(new Transcode())
->setProfile(new TranscodeProfiles\X264_720p())
->setStatus('transcoding')
->setLastEventTimestampMs(round(microtime(true) * 1000) - 700000)
]));
$this->getStatus($video)
->shouldReturn('failed');
}
public function it_should_return_completed_status()
{
$video = new Video();
$video->set('guid', '123');
$this->repository->getList([ 'guid' => '123' ])
->shouldBeCalled()
->willReturn(new Response([
(new Transcode())
->setProfile(new TranscodeProfiles\X264_360p())
->setStatus('completed')
->setLastEventTimestampMs(round(microtime(true) * 1000)),
(new Transcode())
->setProfile(new TranscodeProfiles\X264_720p())
->setStatus('completed')
->setLastEventTimestampMs(round(microtime(true) * 1000))
]));
$this->getStatus($video)
->shouldReturn('completed');
}
public function it_should_return_failed_status()
{
$video = new Video();
$video->set('guid', '123');
$this->repository->getList([ 'guid' => '123' ])
->shouldBeCalled()
->willReturn(new Response([
(new Transcode())
->setProfile(new TranscodeProfiles\X264_360p())
->setStatus('failed')
->setLastEventTimestampMs(round(microtime(true) * 1000)),
(new Transcode())
->setProfile(new TranscodeProfiles\X264_720p())
->setStatus('failed')
->setLastEventTimestampMs(round(microtime(true) * 1000))
]));
$this->getStatus($video)
->shouldReturn('failed');
}
}
<?php
namespace Spec\Minds\Core\Media\Video\Transcoder\TranscodeStorage;
use Minds\Core\Media\Video\Transcoder\TranscodeStorage\S3Storage;
use Minds\Core\Media\Video\Transcoder\Transcode;
use Minds\Core\Media\Video\Transcoder\TranscodeProfiles;
use Psr\Http\Message\RequestInterface;
use Aws\S3\S3Client;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class S3StorageSpec extends ObjectBehavior
{
private $s3;
public function let(S3Client $s3)
{
$this->beConstructedWith(null, $s3);
$this->s3 = $s3;
}
public function it_is_initializable()
{
$this->shouldHaveType(S3Storage::class);
}
public function it_should_upload_file(Transcode $transcode)
{
$transcode->getGuid()
->willReturn(123);
$transcode->getProfile()
->willReturn(new TranscodeProfiles\X264_360p());
$this->s3->putObject(Argument::that(function ($args) {
return true;
}))
->shouldBeCalled()
->willReturn(true);
$this->add($transcode, tempnam(sys_get_temp_dir(), 'my-fake-path'));
}
public function it_should_return_a_signed_url_for_client_side_uploads(
Transcode $transcode,
\Aws\CommandInterface $cmd,
RequestInterface $request
) {
$transcode->getGuid()
->willReturn(123);
$transcode->getProfile()
->willReturn(new TranscodeProfiles\Source());
$this->s3->getCommand('PutObject', [
'Bucket' => 'cinemr',
'Key' => "/123/source",
])
->shouldBeCalled()
->willReturn($cmd);
$this->s3->createPresignedRequest(Argument::any(), Argument::any())
->willReturn($request);
$request->getUri()
->willReturn('aws-signed-url');
$this->getClientSideUploadUrl($transcode)
->shouldReturn('aws-signed-url');
}
public function it_should_download_file(Transcode $transcode)
{
$transcode->getGuid()
->willReturn(123);
$transcode->getProfile()
->willReturn(new TranscodeProfiles\Source());
$this->downloadToTmp($transcode)
->shouldContain("123-source");
}
}
......@@ -114,6 +114,11 @@ class Mock
return (int) $this->a;
}
public function microtime()
{
return (int) $this->a;
}
public function toInt()
{
return (int) $this->a;
......
Please register or to comment