commit 8b7e68d3bd0f4d0cc22f89bee822a0cd3064af33 Author: smarcet Date: Fri Oct 9 18:37:59 2020 -0300 Mux export integration Create command summit:presentation-materials-mux-assets {summit_id} {mounting_folder?} {event_id?} Change-Id: If1ac9b315ea7ede64109e21d0c9dd0468b6c7e2c Signed-off-by: smarcet diff --git a/.env.example b/.env.example index a881d4d..4d2e990 100644 --- a/.env.example +++ b/.env.example @@ -167,4 +167,7 @@ RABBITMQ_SSL_LOCALCERT=/certs/rabbit/client-cert-osf.pem RABBITMQ_SSL_LOCALKEY=/certs/rabbit/client-key-osf.pem RABBITMQ_SSL_VERIFY_PEER=false -DROPBOX_ACCESS_TOKEN= \ No newline at end of file +DROPBOX_ACCESS_TOKEN= + +MUX_TOKEN_ID= +MUX_TOKEN_SECRET= \ No newline at end of file diff --git a/Libs/Utils/ITransactionService.php b/Libs/Utils/ITransactionService.php index 0c8d049..7b52a38 100644 --- a/Libs/Utils/ITransactionService.php +++ b/Libs/Utils/ITransactionService.php @@ -27,5 +27,5 @@ interface ITransactionService * * @throws \Exception */ - public function transaction(Closure $callback, int $isolationLevel); + public function transaction(Closure $callback, int $isolationLevel = 2); } \ No newline at end of file diff --git a/app/Console/Commands/PresentationMaterialsCreateMUXAssetsCommand.php b/app/Console/Commands/PresentationMaterialsCreateMUXAssetsCommand.php new file mode 100644 index 0000000..9be72e7 --- /dev/null +++ b/app/Console/Commands/PresentationMaterialsCreateMUXAssetsCommand.php @@ -0,0 +1,72 @@ +argument('summit_id'); + + $event_id = $this->argument('event_id'); + + if(empty($summit_id)) + throw new \InvalidArgumentException("summit_id is required"); + + $mountingFolder = $this->argument('mounting_folder'); + if(empty($mountingFolder)) + $mountingFolder = Config::get('mediaupload.mounting_folder'); + + Log::debug(sprintf("starting to process published presentations for summit id %s mountingFolder %s event id %s", $summit_id, $mountingFolder, $event_id)); + $this->info(sprintf("starting to process published presentations for summit id %s mountingFolder %s event id %s", $summit_id, $mountingFolder, $event_id)); + + if(empty($event_id)) { + $service->processPublishedPresentationFor(intval($summit_id), $mountingFolder); + return; + } + + $service->processEvent(intval($event_id), $mountingFolder); + } +} \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 70ad923..436625d 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -11,6 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\Console\Commands\PresentationMaterialsCreateMUXAssetsCommand; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Support\Facades\App; @@ -41,6 +43,7 @@ class Kernel extends ConsoleKernel \App\Console\Commands\SummitForwardXDays::class, \App\Console\Commands\SummitEmailFlowEventSeederCommand::class, \App\Console\Commands\SummitEmailFlowTypeSeederCommand::class, + \App\Console\Commands\PresentationMaterialsCreateMUXAssetsCommand::class, ]; /** diff --git a/app/ModelSerializers/Summit/Presentation/AdminPresentationCSVSerializer.php b/app/ModelSerializers/Summit/Presentation/AdminPresentationCSVSerializer.php index ea9ba82..6667d0a 100644 --- a/app/ModelSerializers/Summit/Presentation/AdminPresentationCSVSerializer.php +++ b/app/ModelSerializers/Summit/Presentation/AdminPresentationCSVSerializer.php @@ -43,7 +43,7 @@ final class AdminPresentationCSVSerializer extends AdminPresentationSerializer $values['video'] = ''; $values['public_video'] = ''; foreach ($presentation->getMediaUploads() as $mediaUpload) { - if(str_contains(strtolower($mediaUpload->getMediaUploadType()->getType()->getName()), "video")) { + if($mediaUpload->getMediaUploadType()->isVideo()) { $media_upload_csv = SerializerRegistry::getInstance()->getSerializer($mediaUpload, $serializerType)->serialize(AbstractSerializer::filterExpandByPrefix($expand, 'media_uploads'));; if(!isset($media_upload_csv['private_url']) || !isset($media_upload_csv['filename'])){ Log::warning(sprintf("AdminPresentationCSVSerializer::serialize can not process media upload %s", json_encode($media_upload_csv))); diff --git a/app/Models/Foundation/Summit/Events/Presentations/Materials/PresentationMediaUpload.php b/app/Models/Foundation/Summit/Events/Presentations/Materials/PresentationMediaUpload.php index 8531d89..037824a 100644 --- a/app/Models/Foundation/Summit/Events/Presentations/Materials/PresentationMediaUpload.php +++ b/app/Models/Foundation/Summit/Events/Presentations/Materials/PresentationMediaUpload.php @@ -15,6 +15,7 @@ use App\Models\Utils\IStorageTypesConstants; use Doctrine\ORM\Mapping AS ORM; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Log; /** * @ORM\Entity @@ -107,18 +108,24 @@ class PresentationMediaUpload extends PresentationMaterial /** * @param string $storageType + * @param string|null $mountingFolder * @return string */ - public function getRelativePath(string $storageType = IStorageTypesConstants::PublicType):string { - return sprintf('%s/%s', $this->getPath($storageType), $this->getFilename()); + public function getRelativePath(string $storageType = IStorageTypesConstants::PublicType, ?string $mountingFolder = null):string { + return sprintf('%s/%s', $this->getPath($storageType, $mountingFolder), $this->getFilename()); } /** * @param string $storageType + * @param string|null $mountingFolder * @return string */ - public function getPath(string $storageType = IStorageTypesConstants::PublicType): string { - $mountingFolder = Config::get('mediaupload.mounting_folder'); + public function getPath(string $storageType = IStorageTypesConstants::PublicType, ?string $mountingFolder = null): string { + if(empty($mountingFolder)) + $mountingFolder = Config::get('mediaupload.mounting_folder'); + + Log::debug(sprintf("PresentationMediaUpload::getPath storageType %s mountingFolder %s", $storageType, $mountingFolder)); + $summit = $this->getPresentation()->getSummit(); $presentation = $this->getPresentation(); $format = $storageType == IStorageTypesConstants::PublicType ? '%s/%s/%s': '%s/'.IStorageTypesConstants::PrivateType.'/%s/%s'; diff --git a/app/Models/Foundation/Summit/Events/SummitEvent.php b/app/Models/Foundation/Summit/Events/SummitEvent.php index d938e52..98fe226 100644 --- a/app/Models/Foundation/Summit/Events/SummitEvent.php +++ b/app/Models/Foundation/Summit/Events/SummitEvent.php @@ -218,6 +218,18 @@ class SummitEvent extends SilverstripeBaseModel protected $streaming_url; /** + * @ORM\Column(name="MuxAssetID", type="string") + * @var string + */ + protected $mux_asset_id; + + /** + * @ORM\Column(name="MuxPlaybackID", type="string") + * @var string + */ + protected $mux_playback_id; + + /** * @ORM\Column(name="EtherpadLink", type="string") * @var string */ @@ -1281,4 +1293,37 @@ class SummitEvent extends SilverstripeBaseModel } return null; } + + /** + * @return string + */ + public function getMuxAssetId(): ?string + { + return $this->mux_asset_id; + } + + /** + * @param string $mux_asset_id + */ + public function setMuxAssetId(string $mux_asset_id): void + { + $this->mux_asset_id = $mux_asset_id; + } + + /** + * @return string + */ + public function getMuxPlaybackId(): ?string + { + return $this->mux_playback_id; + } + + /** + * @param string $mux_playback_id + */ + public function setMuxPlaybackId(string $mux_playback_id): void + { + $this->mux_playback_id = $mux_playback_id; + } + } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/MediaUploads/SummitMediaUploadType.php b/app/Models/Foundation/Summit/MediaUploads/SummitMediaUploadType.php index a28d9b7..2d651dc 100644 --- a/app/Models/Foundation/Summit/MediaUploads/SummitMediaUploadType.php +++ b/app/Models/Foundation/Summit/MediaUploads/SummitMediaUploadType.php @@ -272,4 +272,8 @@ class SummitMediaUploadType extends SilverstripeBaseModel return ($this->private_storage_type != IStorageTypesConstants::None || $this->public_storage_type != IStorageTypesConstants::None); } + public function isVideo():bool{ + return str_contains(strtolower($this->getType()->getName()), "video"); + } + } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Repositories/ISummitEventRepository.php b/app/Models/Foundation/Summit/Repositories/ISummitEventRepository.php index 3eb3b57..c741144 100644 --- a/app/Models/Foundation/Summit/Repositories/ISummitEventRepository.php +++ b/app/Models/Foundation/Summit/Repositories/ISummitEventRepository.php @@ -65,4 +65,10 @@ interface ISummitEventRepository extends IBaseRepository * @return mixed */ public function getPublishedEventsBySummitNotInExternalIds(Summit $summit, array $external_ids); + + /** + * @param int $summit_id, + * @return array + */ + public function getPublishedEventsIdsBySummit(int $summit_id):array; } \ No newline at end of file diff --git a/app/Repositories/Summit/DoctrineSummitEventRepository.php b/app/Repositories/Summit/DoctrineSummitEventRepository.php index 727f1f5..1b83fdd 100644 --- a/app/Repositories/Summit/DoctrineSummitEventRepository.php +++ b/app/Repositories/Summit/DoctrineSummitEventRepository.php @@ -370,6 +370,24 @@ final class DoctrineSummitEventRepository } /** + * @param int $summit_id, + * @return array + */ + public function getPublishedEventsIdsBySummit(int $summit_id):array + { + $query = $this->getEntityManager() + ->createQueryBuilder() + ->select("e.id") + ->from($this->getBaseEntity(), "e") + ->join('e.summit', 's', Join::WITH, " s.id = :summit_id") + ->where('e.published = 1') + ->setParameter('summit_id', $summit_id); + + $res = $query->getQuery()->getArrayResult(); + return array_column($res, 'id'); + } + + /** * @param PagingInfo $paging_info * @param Filter|null $filter * @param Order|null $order diff --git a/app/Services/FileSystem/Dropbox/DropboxAdapter.php b/app/Services/FileSystem/Dropbox/DropboxAdapter.php new file mode 100644 index 0000000..5fb8d82 --- /dev/null +++ b/app/Services/FileSystem/Dropbox/DropboxAdapter.php @@ -0,0 +1,52 @@ +client; + try { + // default visibility is RequestedVisibility.public. + $res = $client->createSharedLinkWithSettings($path); + return $res['url']; + } + catch (BadRequestException $ex){ + if($ex->dropboxCode === 'shared_link_already_exists') + { + try { + $res = $client->listSharedLinks($path); + foreach ($res as $entry) { + if($entry['path_lower'] === strtolower($path) ) + return $entry['url']; + } + } + catch (Exception $ex){ + Log::warning($ex); + } + } + } + catch (Exception $ex){ + Log::warning($ex); + } + return '#'; + } +} diff --git a/app/Services/FileSystem/Dropbox/DropboxServiceProvider.php b/app/Services/FileSystem/Dropbox/DropboxServiceProvider.php index fe2f3f2..69323f5 100644 --- a/app/Services/FileSystem/Dropbox/DropboxServiceProvider.php +++ b/app/Services/FileSystem/Dropbox/DropboxServiceProvider.php @@ -15,7 +15,7 @@ use Illuminate\Support\Facades\Storage; use Illuminate\Support\ServiceProvider; use League\Flysystem\Filesystem; use Spatie\Dropbox\Client as DropboxClient; -use Spatie\FlysystemDropbox\DropboxAdapter; +use App\Services\FileSystem\Dropbox\DropboxAdapter; /** * Class DropboxServiceProvider * @package App\Services\FileSystem\Dropbox diff --git a/app/Services/Model/IPresentationVideoMediaUploadProcessor.php b/app/Services/Model/IPresentationVideoMediaUploadProcessor.php new file mode 100644 index 0000000..f13f2b3 --- /dev/null +++ b/app/Services/Model/IPresentationVideoMediaUploadProcessor.php @@ -0,0 +1,35 @@ +summit_repository = $summit_repository; + $this->event_repository = $event_repository; + + $mux_user = Config::get("mux.user", null); + $mux_password = Config::get("mux.password", null); + + if(empty($mux_user)){ + throw new \InvalidArgumentException("missing setting mux.user"); + } + if(empty($mux_password)){ + throw new \InvalidArgumentException("missing setting mux.password"); + } + + // Authentication Setup + $config = MuxConfig::getDefaultConfiguration() + ->setUsername($mux_user) + ->setPassword($mux_password); + + // API Client Initialization + $this->assets_api = new MuxAssetApi( + new GuzzleHttpClient, + $config + ); + } + + /** + * @param int $summit_id + * @param string|null $mountingFolder + * @return int + * @throws \Exception + */ + public function processPublishedPresentationFor(int $summit_id, ?string $mountingFolder = null): int + { + Log::debug(sprintf("PresentationVideoMediaUploadProcessor::processPublishedPresentationFor summit id %s mountingFolder %s", $summit_id, $mountingFolder)); + $event_ids = $this->tx_service->transaction(function() use($summit_id){ + return $this->event_repository->getPublishedEventsIdsBySummit($summit_id); + }); + + foreach($event_ids as $event_id){ + Log::warning(sprintf("PresentationVideoMediaUploadProcessor::processPublishedPresentationFor processing event %s", $event_id)); + $this->processEvent(intval($event_id), $mountingFolder); + } + + return count($event_ids); + } + + /** + * @param int $event_id + * @param string|null $mountingFolder + * @return bool + */ + public function processEvent(int $event_id, ?string $mountingFolder):bool{ + try { + return $this->tx_service->transaction(function () use ($event_id, $mountingFolder) { + try { + $event = $this->event_repository->getByIdExclusiveLock($event_id); + if (is_null($event) || !$event instanceof Presentation) { + Log::warning(sprintf("PresentationVideoMediaUploadProcessor::processEvent event %s not found", $event_id)); + return false; + } + + if(!$event->isPublished()){ + Log::warning(sprintf("PresentationVideoMediaUploadProcessor::processEvent event %s not published", $event_id)); + return false; + } + + Log::debug(sprintf("PresentationVideoMediaUploadProcessor::processEvent processing event %s (%s)", $event->getTitle(), $event_id)); + + if(!empty($event->getMuxAssetId())){ + Log::warning(sprintf("PresentationVideoMediaUploadProcessor::processEvent event %s already has assigned an asset id %s", $event_id, $event->getMuxAssetId())); + return false; + } + + $has_video = false; + foreach($event->getMediaUploads() as $mediaUpload){ + + if($mediaUpload->getMediaUploadType()->isVideo()){ + if($has_video){ + Log::warning(sprintf("PresentationVideoMediaUploadProcessor::processEvent event %s processing media upload %s (%s) already has a video processed!.", $event_id, $mediaUpload->getId(), $mediaUpload->getFilename())); + continue; + } + Log::debug(sprintf("PresentationVideoMediaUploadProcessor::processEvent event %s processing media upload %s", $event_id, $mediaUpload->getId())); + $has_video = true; + + $strategy = FileDownloadStrategyFactory::build($mediaUpload->getMediaUploadType()->getPrivateStorageType()); + if (!is_null($strategy)) { + $assetUrl = $strategy->getUrl($mediaUpload->getRelativePath(IStorageTypesConstants::PrivateType, $mountingFolder)); + Log::debug(sprintf("PresentationVideoMediaUploadProcessor::processEvent event %s processing media upload %s got asset url %s", $event_id, $mediaUpload->getId(), $assetUrl)); + + // Create Asset Request + $input = new MuxInputSettings(["url" => $assetUrl]); + $createAssetRequest = new MuxCreateAssetRequest(["input" => $input, "playback_policy" => [MuxPlaybackPolicy::PUBLIC_PLAYBACK_POLICY] ]); + // Ingest + $result = $this->assets_api->createAsset($createAssetRequest); + + // Print URL + $playback_id = $result->getData()->getPlaybackIds()[0]->getId(); + $streaming_url = sprintf("https://stream.mux.com/%s.m3u8", $playback_id); + $asset_id = $result->getData()->getId(); + Log::debug(sprintf("PresentationVideoMediaUploadProcessor::processEvent event %s Playback URL: %s assset id %s", $event_id, $streaming_url, $asset_id)); + + $event->setStreamingUrl($streaming_url); + $event->setMuxAssetId($asset_id); + $event->setMuxPlaybackId($playback_id); + } + } + } + } catch (\Exception $ex) { + Log::warning($ex); + throw $ex; + } + return true; + }); + } + catch (\Exception $ex) { + Log::error($ex); + return false; + } + } +} \ No newline at end of file diff --git a/app/Services/ModelServicesProvider.php b/app/Services/ModelServicesProvider.php index 51e0984..08aeae8 100644 --- a/app/Services/ModelServicesProvider.php +++ b/app/Services/ModelServicesProvider.php @@ -111,6 +111,8 @@ use App\Services\Model\ICalendarSyncWorkRequestPreProcessor; use App\Services\Model\IMemberActionsCalendarSyncProcessingService; use App\Services\Model\AdminActionsCalendarSyncProcessingService; use App\Services\Model\IAdminActionsCalendarSyncProcessingService; +use App\Services\Model\IPresentationVideoMediaUploadProcessor; +use App\Services\Model\Imp\PresentationVideoMediaUploadProcessor; /*** * Class ModelServicesProvider * @package services @@ -377,6 +379,12 @@ final class ModelServicesProvider extends ServiceProvider ISummitMediaUploadTypeService::class, SummitMediaUploadTypeService::class ); + + App::singleton + ( + IPresentationVideoMediaUploadProcessor::class, + PresentationVideoMediaUploadProcessor::class + ); } /** @@ -436,6 +444,7 @@ final class ModelServicesProvider extends ServiceProvider ISummitAdministratorPermissionGroupService::class, ISummitMediaFileTypeService::class, ISummitMediaUploadTypeService::class, + IPresentationVideoMediaUploadProcessor::class ]; } } \ No newline at end of file diff --git a/composer.json b/composer.json index 6c98653..545fc7a 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "laravel/tinker": "^1.0", "league/csv": "^9.6", "league/oauth2-client": "^2.4", + "muxinc/mux-php": "^0.5.0", "php-amqplib/php-amqplib": "^2.11", "php-opencloud/openstack": "dev-master", "predis/predis": "1.0.*", diff --git a/composer.lock b/composer.lock index 7896978..8288e5a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "a441f3fcf95ec732067dc7b61c451a5b", + "content-hash": "2d35492694cc690aadba8344d827c267", "packages": [ { "name": "bacon/bacon-qr-code", @@ -2877,6 +2877,60 @@ "time": "2020-05-22T07:31:27+00:00" }, { + "name": "muxinc/mux-php", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/muxinc/mux-php.git", + "reference": "b9894ee7003f2caf2e74366c4dfb19736832077f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/muxinc/mux-php/zipball/b9894ee7003f2caf2e74366c4dfb19736832077f", + "reference": "b9894ee7003f2caf2e74366c4dfb19736832077f", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^6.2", + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.12", + "squizlabs/php_codesniffer": "~2.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "MuxPhp\\": "MuxPhp/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mux SDK team", + "email": "sdks@mux.com", + "homepage": "https://mux.com" + } + ], + "description": "Official Mux API wrapper for PHP projects, supporting both Mux Data and Mux Video. Not familiar with Mux? Check out https://mux.com/ for more information.", + "homepage": "https://mux.com", + "keywords": [ + "api", + "php", + "rest", + "sdk", + "streaming", + "video" + ], + "time": "2020-09-10T18:53:57+00:00" + }, + { "name": "nesbot/carbon", "version": "1.26.6", "source": { @@ -7320,6 +7374,7 @@ "keywords": [ "tokenizer" ], + "abandoned": true, "time": "2019-09-17T06:23:10+00:00" }, { diff --git a/config/mux.php b/config/mux.php new file mode 100644 index 0000000..7919632 --- /dev/null +++ b/config/mux.php @@ -0,0 +1,19 @@ + env('MUX_TOKEN_ID', null), + 'password' => env('MUX_TOKEN_SECRET', null), +]; \ No newline at end of file diff --git a/database/migrations/model/Version20200928132323.php b/database/migrations/model/Version20200928132323.php index 4d6b02d..741926c 100644 --- a/database/migrations/model/Version20200928132323.php +++ b/database/migrations/model/Version20200928132323.php @@ -39,6 +39,11 @@ class Version20200928132323 extends AbstractMigration */ public function down(Schema $schema) { - + $builder = new Builder($schema); + if($schema->hasTable("PresentationMediaUpload") && $builder->hasColumn("PresentationMediaUpload","LegacyPathFormat") ) { + $builder->table('PresentationMediaUpload', function (Table $table) { + $table->dropColumn('LegacyPathFormat'); + }); + } } } diff --git a/database/migrations/model/Version20201008203936.php b/database/migrations/model/Version20201008203936.php new file mode 100644 index 0000000..f2e922e --- /dev/null +++ b/database/migrations/model/Version20201008203936.php @@ -0,0 +1,51 @@ +hasTable("SummitEvent") && !$builder->hasColumn("SummitEvent","MuxPlaybackID") ) { + $builder->table('SummitEvent', function (Table $table) { + $table->text('MuxPlaybackID')->setDefault(null)->setNotnull(false); + $table->text('MuxAssetID')->setDefault(null)->setNotnull(false); + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $builder = new Builder($schema); + if($schema->hasTable("SummitEvent") && $builder->hasColumn("SummitEvent","MuxPlaybackID") ) { + $builder->table('SummitEvent', function (Table $table) { + $table->dropColumn('MuxPlaybackID'); + $table->dropColumn('MuxAssetID'); + }); + } + } +}