Commit 0544ae82 authored by Mark Harding's avatar Mark Harding
Browse files

(feat): transcoder notification - #1187

parent 2b58ff84
Loading
Loading
Loading
Loading
+78 −0
Original line number Diff line number Diff line
<?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,
        ]);
    }
}
+12 −11
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@
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;

@@ -21,6 +22,9 @@ class Manager
        TranscodeProfiles\Webm_1080p::class,
    ];

    /** @var int */
    const TRANSCODER_TIMEOUT_SECS = 600; // 10 minutes with not progress

    /** @var Repository */
    private $repository;

@@ -33,12 +37,16 @@ class Manager
    /** @var TranscodeExecutors\TranscodeExecutorInterfsce */
    private $transcodeExecutor;

    public function __construct($repository = null, $queueDelegate = null, $transcodeStorage = null, $transcodeExecutor = null)
    /** @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();
    }

    /**
@@ -110,7 +118,8 @@ class Manager
                $transcode = new Transcode();
                $transcode
                    ->setVideo($video)
                    ->setProfile(new $profile);
                    ->setProfile(new $profile)
                    ->setStatus(TranscodeStates::CREATED);
                // Add the transcode to database and queue
                $this->add($transcode);
            } catch (TranscodeProfiles\UnavailableTranscodeProfileException $e) {
@@ -173,14 +182,6 @@ class Manager
            $this->update($transcode, [ 'progress', 'status' ]);
        }

        // Was this the last transcode to complete?
        // if ($this->isLastToTrancode($transcode)) {
        //     // Sent a notification to the user saying the transcode is completed
        // }
        $this->notificationDelegate->onTranscodeCompleted($transcode);
    }

    // protected function isLastToTrancode(Transcode $transcode): bool
    // {

    // }
}
+21 −6
Original line number Diff line number Diff line
@@ -38,7 +38,14 @@ class Repository
            'status' => null,
        ], $opts);

        $statement = "SELECT * FROM video_transcodes";
        $statement = "SELECT 
            guid,
            profile_id,
            progress,
            status,
            last_event_timestamp_ms,
            bytes
            FROM video_transcodes";

        $where = [];
        $values = [];
@@ -88,7 +95,14 @@ class Repository
        $urn = Urn::_($urn);
        list($guid, $profile) = explode('-', $urn->getNss());

        $statement = "SELECT * FROM video_transcodes
        $statement = "SELECT 
            guid,
            profile_id,
            progress,
            status,
            last_event_timestamp_ms,
            bytes
            FROM video_transcodes
            WHERE guid = ?
            AND profile = ?";
        $values = [
@@ -119,10 +133,11 @@ class Repository
     */
    public function add(Transcode $transcode): bool
    {
        $statement = "INSERT INTO video_transcodes (guid, profile_id) VALUES (?, ?)";
        $statement = "INSERT INTO video_transcodes (guid, profile_id, status) VALUES (?, ?)";
        $values = [
            new Bigint($transcode->getGuid()),
            $transcode->getProfile()->getId(),
            $transcode->getStatus(),
        ];

        $prepared = new Custom();
@@ -235,11 +250,11 @@ class Repository
        $transcode = new Transcode();
        $transcode->setGuid((string) $row['guid'])
            ->setProfile(TranscodeProfiles\Factory::build((string) $row['profile_id']))
            ->setProgress($row['progress']->value())
            ->setProgress($row['progress'])
            ->setStatus($row['status'])
            ->setLastEventTimestampMs(round($row['last_event_timestamp_ms']->microtime(true) * 1000))
            ->setLength($row['length_secs']->value())
            ->setBytes($row['bytes']->value());
            ->setLengthSecs($row['length_secs'])
            ->setBytes($row['bytes']);
        return $transcode;
    }
}
+81 −0
Original line number Diff line number Diff line
<?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(),
        ]);

        $failures = 0;
        $completed = 0;

        foreach ($transcodes as $transcode) {
            if ($transcode instanceof TranscodeProfiles\Thumbnails) {
                continue; // We skip thumbnails as these are likely to succeed
            }
            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
                    return TranscodeStates::CREATED;
                    break;
                case TranscodeStates::FAILED:
                    ++$failures;
                    // We should allow failures for some transcodes
                    break;
                case TranscodeStates::COMPLETED:
                    ++$completed;
                    // We should allow failures for some transcodes
                    break;
            }
        }

        // If we have more completions then failures the declare completed
        if ($failures < $completed) {
            return TranscodeStates::COMPLETED;
        }

        return TranscodeStates::FAILED; // Our default state is failed?
    }
}
+100 −0
Original line number Diff line number Diff line
<?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);
    }
}
Loading