diff --git a/classes/Tasks/AttachmentTask.php b/classes/Tasks/AttachmentTask.php index 391fbd49..48902f31 100644 --- a/classes/Tasks/AttachmentTask.php +++ b/classes/Tasks/AttachmentTask.php @@ -37,15 +37,27 @@ protected function updateCurrentPost($post_id) { $this->currentTitle = get_post_field('post_title', $post_id); - $thumb = wp_get_attachment_image_src($post_id, 'thumbnail', true); - if (!empty($thumb)) { - $this->currentThumb = $thumb[0]; - $this->isIcon = (($thumb[1] != 150) && ($thumb[2] != 150)); + if (strpos(get_post_mime_type($post_id), 'video/') === 0) { + $thumb = get_the_post_thumbnail_url($post_id, 'thumbnail'); + if (!empty($thumb)) { + $this->currentThumb = $thumb; + $this->isIcon = false; + } else { + $this->currentThumb = null; + $this->isIcon = false; + } } else { - $this->currentThumb = null; - $this->isIcon = false; + $thumb = wp_get_attachment_image_src($post_id, 'thumbnail', true); + if (!empty($thumb)) { + $this->currentThumb = $thumb[0]; + $this->isIcon = (($thumb[1] != 150) && ($thumb[2] != 150)); + } else { + $this->currentThumb = null; + $this->isIcon = false; + } } + $this->save(); } @@ -87,7 +99,9 @@ public function prepare($options = [], $selectedItems = []) { if (!empty($selectedItems) && is_array($selectedItems)) { foreach($selectedItems as $postId) { - $this->addItem($this->filterItem(['id' => $postId], $options)); + if (!$this->addDataForPost($postId, $options)) { + $this->addItem($this->filterItem(['id' => $postId], $options)); + } } } else { $args = [ @@ -135,7 +149,9 @@ public function prepare($options = [], $selectedItems = []) { } foreach($postIds as $postId) { - $this->addItem($this->filterItem(['id' => $postId], $options)); + if (!$this->addDataForPost($postId, $options)) { + $this->addItem($this->filterItem(['id' => $postId], $options)); + } } } @@ -145,4 +161,8 @@ public function prepare($options = [], $selectedItems = []) { return ($this->totalItems > 0); } + protected function addDataForPost($postId, $options):bool { + return false; + } + } diff --git a/classes/Tasks/Task.php b/classes/Tasks/Task.php index 2e2c2041..57274291 100644 --- a/classes/Tasks/Task.php +++ b/classes/Tasks/Task.php @@ -1023,6 +1023,7 @@ public static function markConfirmed() { //region JSON + #[\ReturnTypeWillChange] public function jsonSerialize() { if ($this->state >= self::STATE_COMPLETE) { $remaining = 0; diff --git a/classes/Tasks/TaskSchedule.php b/classes/Tasks/TaskSchedule.php index 42c56c01..65dc60dc 100644 --- a/classes/Tasks/TaskSchedule.php +++ b/classes/Tasks/TaskSchedule.php @@ -383,6 +383,7 @@ public static function hasScheduledTaskOfType($type) { //endregion //region JSON + #[\ReturnTypeWillChange] public function jsonSerialize() { return [ 'id' => $this->id, diff --git a/classes/Tools/Crop/CropTool.php b/classes/Tools/Crop/CropTool.php index a950e976..b146157d 100644 --- a/classes/Tools/Crop/CropTool.php +++ b/classes/Tools/Crop/CropTool.php @@ -127,67 +127,50 @@ private function hookupUI() return $content; },10,2); - add_action( 'wp_enqueue_media', function () { - remove_action('admin_footer', 'wp_print_media_templates'); - - add_action('admin_footer', function(){ - ob_start(); - wp_print_media_templates(); - $result=ob_get_clean(); - echo $result; - - - $sizes=ilab_get_image_sizes(); - $sizeKeys=array_keys($sizes); - - ob_start(); - ?> - - editPageURL($post->ID).'" title="Edit Image">'.__('Edit Image').''; + if (strpos($post->post_mime_type, 'image') === 0) { + $meta = wp_get_attachment_metadata($post->ID); + if (empty(arrayPath($meta, 's3', null))) { + return $actions; + } + + $newaction['ilab_edit_image'] = '' . __('Edit Image') . ''; + return array_merge($actions, $newaction); + } - return array_merge($actions, $newaction); + return $actions; }, 10, 2); - add_action('wp_enqueue_media', function() { - remove_action('admin_footer', 'wp_print_media_templates'); - - add_action('admin_footer', function() { - ob_start(); - wp_print_media_templates(); - $result = ob_get_clean(); - echo $result; - - - ?> - - options_group, $option); } - /** - * Registers an option with a text input UI - * @param $option_name - * @param $title - * @param $settings_slug - * @param null $description - * @param null $placeholder - * @param null $conditions - */ - protected function registerTextFieldSetting($option_name, $title, $settings_slug, $description=null, $placeholder=null, $conditions=null, $default=null) { - add_settings_field($option_name, - $title, - [$this,'renderTextFieldSetting'], - $this->options_page, - $settings_slug, - ['option'=>$option_name, 'description'=>$description, 'placeholder' => $placeholder, 'conditions' => $conditions, 'default' => $default]); - } + /** + * Registers an option with a text input UI + * @param $option_name + * @param $title + * @param $settings_slug + * @param null $description + * @param null $placeholder + * @param null $conditions + */ + protected function registerTextFieldSetting($option_name, $title, $settings_slug, $description=null, $placeholder=null, $conditions=null, $default=null) { + add_settings_field($option_name, + $title, + [$this,'renderTextFieldSetting'], + $this->options_page, + $settings_slug, + ['option'=>$option_name, 'description'=>$description, 'placeholder' => $placeholder, 'conditions' => $conditions, 'default' => $default]); + } - /** - * Renders a text field - * @param $args - */ - public function renderTextFieldSetting($args) { - $value = Environment::Option($args['option']); - if (empty($value) && !empty($args['default'])) { - $value = $args['default']; - } + /** + * Registers an option with a text input UI + * @param $option_name + * @param $title + * @param $settings_slug + * @param null $description + * @param null $placeholder + * @param null $conditions + */ + protected function registerColorFieldSetting($option_name, $title, $settings_slug, $description=null, $conditions=null, $default=null) { + add_settings_field($option_name, + $title, + [$this,'renderColorFieldSetting'], + $this->options_page, + $settings_slug, + ['option'=>$option_name, 'description'=>$description, 'conditions' => $conditions, 'default' => $default]); + } - echo View::render_view('base/fields/text-field.php',[ - 'value' => $value, - 'name' => $args['option'], - 'placeholder' => $args['placeholder'], - 'conditions' => $args['conditions'], - 'description' => (isset($args['description'])) ? $args['description'] : false - ]); - } + /** + * Renders a text field + * @param $args + */ + public function renderTextFieldSetting($args) { + $value = Environment::Option($args['option']); + if (empty($value) && !empty($args['default'])) { + $value = $args['default']; + } + + echo View::render_view('base/fields/text-field.php',[ + 'value' => $value, + 'name' => $args['option'], + 'placeholder' => $args['placeholder'], + 'conditions' => $args['conditions'], + 'description' => (isset($args['description'])) ? $args['description'] : false + ]); + } + + /** + * Renders a text field + * @param $args + */ + public function renderColorFieldSetting($args) { + $value = Environment::Option($args['option']); + if (empty($value) && !empty($args['default'])) { + $value = $args['default']; + } + + echo View::render_view('base/fields/color.php',[ + 'value' => $value, + 'name' => $args['option'], + 'conditions' => $args['conditions'], + 'description' => (isset($args['description'])) ? $args['description'] : false + ]); + } /** * Registers an option with a text input UI diff --git a/classes/Tools/Storage/Driver/Supabase/SupabaseStorage.php b/classes/Tools/Storage/Driver/Supabase/SupabaseStorage.php new file mode 100644 index 00000000..031be69c --- /dev/null +++ b/classes/Tools/Storage/Driver/Supabase/SupabaseStorage.php @@ -0,0 +1,466 @@ +settings = new SupabaseStorageSettings(); + } + //endregion + + //region Static Information Methods + public static function identifier() { + return 'supabase'; + } + + public static function name() { + return 'Supabase Storage (Beta)'; + } + + public static function endpoint() { + return null; + } + + public static function pathStyleEndpoint() { + return null; + } + + public static function defaultRegion() { + return null; + } + + public static function forcedRegion() { + return null; + } + + public static function bucketLink($bucket) { + return null; + } + + public function pathLink($bucket, $key) { + return null; + } + //endregion + + //region Enabled/Options + public function usesSignedURLs($type = null) { + return false; + } + + public function supportsDirectUploads() { + return false; + } + + public function supportsWildcardDirectUploads() { + return false; + } + + public function supportsBrowser() { + return true; + } + //endregion + + //region API Requests + + /** + * @param $method + * @param $path + * @param $data + * @param $contentType + * + * @return \MediaCloud\Vendor\Psr\Http\Message\ResponseInterface + * @throws \MediaCloud\Vendor\GuzzleHttp\Exception\GuzzleException + */ + protected function request($method, $path, $data = [], $contentType = 'application/json') { + $handler = new CurlHandler(); + $stack = HandlerStack::create($handler); + + $c = new Client([ + 'base_uri' => $this->settings->storageUrl.'/storage/v1/', + 'verify' => false, + 'handler' => $stack + ]); + + $args = [ + 'headers' => [ + 'Authorization' => "Bearer {$this->settings->key}", + 'apiKey' => "{$this->settings->key}", + 'Content-Type' => $contentType, + ] + ]; + + if ($data) { + if ($contentType === 'application/json') { + $args['body'] = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + } else { + $args['body'] = $data; + } + } + + if (MCLOUD_DEBUGGING) { + $args['curl'] = [ + CURLOPT_PROXY => 'host.docker.internal', + CURLOPT_PROXYPORT => 8888, + ]; + + if ($contentType === 'application/json') { + $tapMiddleware = Middleware::tap(function($request) { + Logger::info("Guzzle Debug:\n".$request->getBody(), [], __METHOD__, __LINE__); + }); + $args['handler'] = $tapMiddleware($handler); + } + } + + return $c->request($method, $path, $args); + } + //endregion + + //region Settings related functions + + /** + * @param ErrorCollector|null $errorCollector + * @return bool|void + */ + public function validateSettings($errorCollector = null) { + delete_option('mcloud-storage-supabase-settings-error'); + $this->settings->settingsError = false; + + $valid = false; + + try { + if($this->enabled()) { + $res = $this->request('GET', 'bucket/'.$this->settings->bucket); + if ($res->getStatusCode() !== 200) { + Logger::error("Error validating supabase storage settings. ".$res->get_error_message(), [], __METHOD__, __LINE__); + $valid = false; + } else { + $valid = true; + } + + if(!$valid) { + $this->settings->settingsError = true; + update_option('mcloud-storage-supabase-settings-error', true); + } + } else { + if ($errorCollector) { + $errorCollector->addError("Supabase settings are missing or incorrect."); + } + } + } catch (\Exception $ex) { + } + + return $valid; + } + + public function settings() { + return $this->settings; + } + + public function enabled() { + if(!($this->settings->key && $this->settings->storageUrl && $this->settings->bucket)) { + $adminUrl = admin_url('admin.php?page=media-cloud-settings&tab=storage'); + NoticeManager::instance()->displayAdminNotice('error', "To start using Cloud Storage, you will need to supply your Supabase credentials..", true, 'ilab-cloud-storage-setup-warning', 'forever'); + + return false; + } + + if($this->settings->settingsError) { + NoticeManager::instance()->displayAdminNotice('error', 'Your Supabase settings are incorrect or the bucket does not exist. Please verify your settings and update them.'); + + return false; + } + + return true; + } + + public function settingsError() { + return $this->settings->settingsError; + } + //endregion + + //region File Functions + + public function bucket() { + return $this->settings->bucket; + } + + public function region() { + return null; + } + + public function isUsingPathStyleEndPoint() { + return false; + } + + public function acl($key) { + return null; + } + + public function insureACL($key, $acl) { + } + + public function updateACL($key, $acl) { + } + + public function canUpdateACL() { + return false; + } + + public function exists($key) { + try { + $res = $this->request('HEAD', 'object/public/'.trailingslashit($this->settings->bucket).$key); + return $res->getStatusCode() < 400; + } catch (\Exception $ex) { + return false; + } + } + + public function copy($sourceKey, $destKey, $acl, $mime = false, $cacheControl = false, $expires = false) { + try { + $this->request('POST', 'object/copy', [ + 'bucketId' => $this->settings->bucket, + 'sourceKey' => $sourceKey, + 'destKey' => $destKey, + ]); + } catch (\Exception $ex) { + Logger::error("Error copying files. ".$ex->getMessage(), [], __METHOD__, __LINE__); + } + } + + public function upload($key, $fileName, $acl, $cacheControl=null, $expires=null, $contentType=null, $contentEncoding=null, $contentLength=null, $tries = 1) { + try { + Logger::startTiming("Start Upload", [], __METHOD__, __LINE__); + if (empty($contentType)) { + $mime = wp_check_filetype_and_ext($fileName, pathinfo($fileName, PATHINFO_BASENAME)); + $contentType = !empty($mime['type']) ? $mime['type'] : 'application/octet-stream'; + } + $res = $this->request('POST', 'object/'.trailingslashit($this->settings->bucket).$key, fopen($fileName, 'r'), $contentType); + Logger::endTiming("End Upload", [], __METHOD__, __LINE__); + } catch (\Exception $ex) { + Logger::error("Error uploading file to Supabase. ".$ex->getMessage(), [], __METHOD__, __LINE__); + } + } + + public function delete($key) { + try { + $this->request('DELETE', 'object/'.untrailingslashit($this->settings->bucket), [ + 'prefixes' => [ltrim($key, '/')], + ]); + } catch (\Exception $ex) { + Logger::error("Error deleting file from Supabase. ".$ex->getMessage(), [], __METHOD__, __LINE__); + } + } + + public function createDirectory($key) { + } + + public function deleteDirectory($key) { + $files = $this->ls($key, '/', -1, null, true); + foreach($files['files'] as $file) { + $this->delete($file); + } + } + + public function dir($path = '', $delimiter = '/', $limit = -1, $next = null) { + try { + $next = $next ?? intval(0); + $res = $this->request('POST', 'object/list/'.untrailingslashit($this->settings->bucket), [ + 'limit' => $limit, + 'offset' => intval($next), + 'prefix' => ltrim($path, '/'), + ]); + + if ($res->getStatusCode() === 200) { + $data = json_decode($res->getBody(), true); + $files = []; + $rootFileCount = 0; + + foreach($data as $datum) { + $rootFileCount++; + if (empty($datum['id'])) { + if ($datum['name'] === '.emptyFolderPlaceholder') { + continue; + } + + $files[] = new StorageFile('DIR', trailingslashit($path).trailingslashit($datum['name'])); + } else { + if ($datum['name'] === '.emptyFolderPlaceholder') { + continue; + } + + $files[] = new StorageFile('FILE', trailingslashit($path).$datum['name'], null, arrayPath($datum, 'created_at', null), arrayPath($datum, 'metadata/size', 0), $this->url(trailingslashit($path).$datum['name'])); + } + } + + return [ + 'next' => $rootFileCount === $limit ? $next + $limit : null, + 'files' => $files, + ]; + + } + } catch (\Exception $ex) { + Logger::error("Error listing files from Supabase. ".$ex->getMessage(), [], __METHOD__, __LINE__); + } + + return [ + 'next' => null, + 'files' => [], + ]; + } + + public function ls($path = '', $delimiter = '/', $limit = -1, $next = null, $recursive = false) { + try { + $next = $next ?? intval(0); + $res = $this->request('POST', 'object/list/'.untrailingslashit($this->settings->bucket), [ + 'limit' => $limit === -1 ? 1000 : $limit, + 'offset' => $next, + 'next' => $next, + 'prefix' => ltrim($path, '/'), + ]); + + if ($res->getStatusCode() === 200) { + $data = json_decode($res->getBody(), true); + $files = []; + $rootFileCount = 0; + + foreach($data as $datum) { + $rootFileCount++; + if (empty($datum['id'])) { + if ($datum['name'] === '.emptyFolderPlaceholder') { + continue; + } + +// $files[] = trailingslashit($path).trailingslashit($datum['name']); + if ($recursive) { + $ls = $this->ls(trailingslashit($path).trailingslashit($datum['name']), $delimiter, $limit, $next, $recursive); + $files = array_merge($files, $ls['files']); + if ($limit === -1) { + while(!empty($ls['next'])) { + $ls = $this->ls(trailingslashit($path).trailingslashit($datum['name']), $delimiter, $limit, $ls['next'], $recursive); + $files = array_merge($files, $ls['files']); + } + } + } + } else { + if ($datum['name'] === '.emptyFolderPlaceholder') { + continue; + } + + $files[] = trailingslashit($path).$datum['name']; + } + } + + return [ + 'next' => $rootFileCount === $limit ? $next + $limit : null, + 'files' => $files, + ]; + + } + } catch (\Exception $ex) { + Logger::error("Error listing files from Supabase. ".$ex->getMessage(), [], __METHOD__, __LINE__); + } + + return [ + 'next' => null, + 'files' => [], + ]; + } + + public function info($key) { + $res = $this->request('HEAD', 'object/public/'.trailingslashit($this->settings->bucket).$key); + if ($res->getStatusCode() !== 200) { + throw new StorageException("Unable to retrieve file info for $key", 400); + } + + $length = arrayPath($res, 'headers/Content-Length', 0); + $type = arrayPath($res, 'headers/Content-Type', 'application/octet-stream'); + $url = $this->url($key); + $size = null; + if(strpos($type, 'image/') === 0) { + $faster = new FasterImage(); + $result = $faster->batch([$url]); + $result = $result[$url]; + $size = $result['size']; + } + + $fileInfo = new FileInfo($key, $url, $url, $length, $type, $size); + return $fileInfo; + } + //endregion + + //region URLs + public function presignedUrl($key, $expiration = 0, $options = []) { + return $this->url($key); + } + + public function url($key, $type = null) { + return trailingslashit($this->settings->storageUrl).'storage/v1/object/public/'.trailingslashit($this->settings->bucket).$key; + } + + public function signedURLExpirationForType($type = null) { + return null; + } + //endregion + + //region Direct Uploads + public function uploadUrl($key, $acl, $mimeType = null, $cacheControl = null, $expires = null) { + } + + public function enqueueUploaderScripts() { + } + //endregion + + + //region Optimization + public function prepareOptimizationInfo() { + return [ + ]; + } + //endregion + public function client() { + return $this; + } +} diff --git a/classes/Tools/Storage/Driver/Supabase/SupabaseStorageSettings.php b/classes/Tools/Storage/Driver/Supabase/SupabaseStorageSettings.php new file mode 100644 index 00000000..56eb226b --- /dev/null +++ b/classes/Tools/Storage/Driver/Supabase/SupabaseStorageSettings.php @@ -0,0 +1,41 @@ + ['mcloud-storage-supabase-url', 'MCLOUD_SUPABASE_URL', null], + 'key' => ['mcloud-storage-supabase-key', 'MCLOUD_SUPABASE_KEY', null], + 'bucket' => ['mcloud-storage-supabase-bucket', 'MCLOUD_SUPABASE_BUCKET', null], + 'settingsError' => ['mcloud-storage-supabase-settings-error', null, false], + ]; +} \ No newline at end of file diff --git a/classes/Tools/Storage/StorageContentHooks.php b/classes/Tools/Storage/StorageContentHooks.php index 3741317a..8df14fc5 100644 --- a/classes/Tools/Storage/StorageContentHooks.php +++ b/classes/Tools/Storage/StorageContentHooks.php @@ -18,8 +18,6 @@ use MediaCloud\Plugin\Tasks\TaskReporter; use MediaCloud\Plugin\Tools\Debugging\DebuggingToolSettings; -use MediaCloud\Plugin\Tools\Storage\Driver\S3\S3StorageSettings; -use MediaCloud\Plugin\Utilities\Environment; use MediaCloud\Plugin\Utilities\Logging\Logger; use function MediaCloud\Plugin\Utilities\anyEmpty; use function MediaCloud\Plugin\Utilities\arrayPath; diff --git a/classes/Tools/Storage/StorageTool.php b/classes/Tools/Storage/StorageTool.php index dfba79ca..127c07a2 100644 --- a/classes/Tools/Storage/StorageTool.php +++ b/classes/Tools/Storage/StorageTool.php @@ -2775,7 +2775,7 @@ private function hookMediaList() add_filter( 'manage_media_columns', function ( $cols ) { $cols["cloud"] = 'Cloud'; return $cols; - } ); + }, PHP_INT_MAX ); add_action( 'manage_media_custom_column', function ( $column_name, $id ) { @@ -2794,13 +2794,31 @@ function ( $column_name, $id ) { $lockIcon = $this->lockIcon(); //ILAB_PUB_IMG_URL.'/ilab-icon-lock.svg'; $lockImg = ( !empty($privacy) && $privacy !== StorageConstants::ACL_PUBLIC_READ ? "" : '' ); - echo "{$lockImg}" ; + echo "
{$lockImg}" ; } } }, - 10, + PHP_INT_MAX - 10, + 2 + ); + add_action( + 'manage_media_custom_column', + function ( $column_name, $id ) { + + if ( $column_name == "cloud" ) { + $meta = wp_get_attachment_metadata( $id ); + if ( empty($meta) && !isset( $meta['s3'] ) ) { + $meta = get_post_meta( $id, 'ilab_s3_info', true ); + } + if ( !empty($meta) && isset( $meta['s3'] ) ) { + echo "
" ; + } + } + + }, + PHP_INT_MAX, 2 ); add_filter( 'bulk_actions-upload', function ( $actions ) { @@ -2886,9 +2904,20 @@ function ( $redirect_to, $action_name, $post_ids ) { if ( get_current_screen()->base == 'upload' ) { ?> diff --git a/classes/Tools/Tool.php b/classes/Tools/Tool.php index bbb28e29..edf95183 100644 --- a/classes/Tools/Tool.php +++ b/classes/Tools/Tool.php @@ -501,6 +501,9 @@ public function registerSettings() { case 'text-field': $this->registerTextFieldSetting($option,$optionInfo['title'],$group, $description, $placeholder, $conditions, isset($optionInfo['default']) ? $optionInfo['default'] : null); break; + case 'color': + $this->registerColorFieldSetting($option, $optionInfo['title'], $group, $description, $conditions, isset($optionInfo['default']) ? $optionInfo['default'] : null); + break; case 'webhook': $this->registerWebhookSetting($option, $optionInfo['title'], $group, !empty($optionInfo['editable']), $description, $conditions, isset($optionInfo['default']) ? $optionInfo['default'] : null); break; diff --git a/classes/Tools/ToolsManager.php b/classes/Tools/ToolsManager.php index 06c2e9c7..8f5fc25d 100644 --- a/classes/Tools/ToolsManager.php +++ b/classes/Tools/ToolsManager.php @@ -196,13 +196,7 @@ function ( $value, $option, $old_value ) { 'mcloud-no-mbstring' ); } - NoticeManager::instance()->displayAdminNotice( - 'info', - "The team behind Media Cloud is launching a new product in April 2022 that's going to change the way you work with media in WordPress. Sign up to be notified when Preflight for WordPress is released.", - true, - 'mcloud-preflight-beta-sales-pitch', - 10 * 365 - ); + // NoticeManager::instance()->displayAdminNotice('info', "The team behind Media Cloud is launching a new product in April 2022 that's going to change the way you work with media in WordPress. Sign up to be notified when Preflight for WordPress is released.", true, 'mcloud-preflight-beta-sales-pitch', 10 * 365); add_action( 'admin_enqueue_scripts', function () { wp_enqueue_script( 'mcloud-admin-js', @@ -287,6 +281,9 @@ protected function setup() foreach ( $this->tools as $key => $tool ) { $tool->setup(); } + if ( is_admin() ) { + $this->hookMediaDetailButtons(); + } do_action( 'mediacloud/tasks/register' ); // MigrationsManager::instance()->displayMigrationErrors(); } @@ -714,13 +711,7 @@ public function addMenus( $networkMode, $networkAdminMenu ) 'https://support.mediacloud.press/' ); } - add_submenu_page( - 'media-cloud', - 'Preflight Beta', - 'Preflight Beta', - 'manage_options', - 'https://preflight.ju.mp' - ); + // add_submenu_page('media-cloud', 'Preflight Beta', 'Preflight Beta', 'manage_options', 'https://preflight.ju.mp'); foreach ( $this->tools as $key => $tool ) { $tool->registerHelpMenu( 'media-cloud', $networkMode, $networkAdminMenu ); } @@ -1077,5 +1068,148 @@ private function handlePinTool() 'link' => admin_url( "admin.php?page=media-cloud-settings&tool={$tool}" ), ] ); } + + //endregion + //region Media Library UI + private function hookMediaDetailButtons() + { + add_action( 'wp_enqueue_media', function () { + remove_action( 'admin_footer', 'wp_print_media_templates' ); + add_action( 'admin_footer', function () { + $mediaButtons = apply_filters( 'mediacloud/ui/media-detail-buttons', [] ); + $mediaLinks = apply_filters( 'mediacloud/ui/media-detail-links', [] ); + $toRemove = apply_filters( 'mediacloud/ui/media-detail-remove', [] ); + $renderedButtons = []; + foreach ( $mediaButtons as $button ) { + $dataAttrs = ''; + if ( isset( $button['data'] ) ) { + foreach ( $button['data'] as $key => $value ) { + $dataAttrs .= "data-{$key}='{$value}' "; + } + } + $thickbox = ( arrayPath( $button, 'thickbox', true ) === true ? 'ilab-thickbox' : '' ); + $classes = arrayPath( $button, 'class', '' ); + $style = arrayPath( $button, 'style', '' ); + + if ( arrayPath( $button, 'button_type', 'link' ) === 'button' ) { + $buttonHTML = ""; + } else { + $buttonHTML = "{$button['label']}"; + } + + if ( $button['type'] !== 'any' ) { + + if ( isset( $button['cloudonly'] ) && $button['cloudonly'] === true ) { + $buttonHTML = "<# if (data.s3 && data.type === '{$button['type']}') { #>\\n{$buttonHTML}\\n<# } #>"; + } else { + $buttonHTML = "<# if (data.type === '{$button['type']}') { #>\\n{$buttonHTML}\\n<# } #>"; + } + + } + $renderedButtons[] = $buttonHTML; + } + $mediaButtonsHTML = implode( ' ', $renderedButtons ); + $renderedLinks = []; + foreach ( $mediaLinks as $link ) { + $classes = arrayPath( $link, 'class', '' ); + $style = arrayPath( $link, 'style', '' ); + $dataAttrs = ''; + if ( isset( $link['data'] ) ) { + foreach ( $link['data'] as $key => $value ) { + $dataAttrs .= "data-{$key}='{$value}' "; + } + } + $thickbox = ( arrayPath( $link, 'thickbox', true ) === true ? 'ilab-thickbox' : '' ); + $linkHTML = "{$link['label']} |"; + if ( $link['type'] !== 'any' ) { + + if ( isset( $link['cloudonly'] ) && $link['cloudonly'] === true ) { + $linkHTML = "<# if (data.s3 && data.type === '{$link['type']}') { #>\\n{$linkHTML}\\n<# } #>"; + } else { + $linkHTML = "<# if (data.type === '{$link['type']}') { #>\\n{$linkHTML}\\n<# } #>"; + } + + } + $renderedLinks[] = $linkHTML; + } + $mediaLinksHTML = implode( '', $renderedLinks ); + $detailLinks = []; + foreach ( $mediaLinks as $link ) { + $classes = arrayPath( $link, 'class', '' ); + $style = arrayPath( $link, 'style', '' ); + $dataAttrs = ''; + if ( isset( $link['data'] ) ) { + foreach ( $link['data'] as $key => $value ) { + $dataAttrs .= "data-{$key}='{$value}' "; + } + } + $linkHTML = "{$link['label']}"; + if ( $link['type'] !== 'any' ) { + $linkHTML = "<# if (data.type === '{$link['type']}') { #>\\n{$linkHTML}\\n<# } #>"; + } + $detailLinks[] = $linkHTML; + } + $mediaDetailLinksHTML = implode( "\\n", $detailLinks ); + ob_start(); + wp_print_media_templates(); + $result = ob_get_clean(); + echo $result ; + ob_start(); + ?> + + ] + * : The maximum number of items to process, default is infinity. + * + * [--offset=] + * : The starting offset to process. Cannot be used with page. + * + * [--page=] + * : The starting offset to process. Page numbers start at 1. Cannot be used with offset. + * + * [--dest=] + * : The destination path on cloud storage to transfer to, for example `/video/`. + * + * [--delete] + * : Deletes the video from Mux after the transfer is complete. + * + * [--local-only] + * : Saves the HLS encoded video to the local server only. + * + * [--skip-transferred] + * : Skips videos that have already been transferred from Mux. + * + * [--order-by=] + * : The field to sort the items to be imported by. Valid values are 'date', 'title' and 'filename'. + * --- + * options: + * - date + * - title + * - filename + * --- + * + * [--order=] + * : The sort order. Valid values are 'asc' and 'desc'. + * --- + * default: asc + * options: + * - asc + * - desc + * --- + * + * @when after_wp_load + * + * @param $args + * @param $assoc_args + * + * @throws \Exception + */ + public function transfer( $args, $assoc_args ) + { + /** @var \Freemius $media_cloud_licensing */ + global $media_cloud_licensing ; + self::Error( "Only available in the Premium version. To upgrade: https://mediacloud.press/pricing/" ); + } + + /** + * Relinks Mux videos that were transferred to the local server or cloud storage. + * + * ## OPTIONS + * + * + * [--limit=] + * : The maximum number of items to process, default is infinity. + * + * [--offset=] + * : The starting offset to process. Cannot be used with page. + * + * [--page=] + * : The starting offset to process. Page numbers start at 1. Cannot be used with offset. + * + * [--order-by=] + * : The field to sort the items to be imported by. Valid values are 'date', 'title' and 'filename'. + * --- + * options: + * - date + * - title + * - filename + * --- + * + * [--order=] + * : The sort order. Valid values are 'asc' and 'desc'. + * --- + * default: asc + * options: + * - asc + * - desc + * --- + * + * @when after_wp_load + * + * @param $args + * @param $assoc_args + * + * @throws \Exception + */ + public function relink( $args, $assoc_args ) + { + /** @var \Freemius $media_cloud_licensing */ + global $media_cloud_licensing ; + self::Error( "Only available in the Premium version. To upgrade: https://mediacloud.press/pricing/" ); + } + + public static function Register() + { + \WP_CLI::add_command( 'mediacloud:video', __CLASS__ ); + } + +} \ No newline at end of file diff --git a/classes/Tools/Video/Driver/Mux/Data/MuxDatabase.php b/classes/Tools/Video/Driver/Mux/Data/MuxDatabase.php index 57f1ea82..204f7145 100644 --- a/classes/Tools/Video/Driver/Mux/Data/MuxDatabase.php +++ b/classes/Tools/Video/Driver/Mux/Data/MuxDatabase.php @@ -17,7 +17,7 @@ * Interface for creating the required tables */ final class MuxDatabase { - const DB_VERSION = '1.0.0'; + const DB_VERSION = '1.0.4'; /** * Insures the additional database tables are installed @@ -54,8 +54,13 @@ protected static function installAssetsTable() { mp4Support int NOT NULL default 0, width int NOT NULL default 0, height int NOT NULL default 0, + isTransferred int NOT NULL default 0, + isDeleted int NOT NULL default 0, aspectRatio VARCHAR(32) NULL, - jsonData text NULL,"; + transferData text NULL, + relatedFiles text NULL, + jsonData text NULL, + "; $rowFormat = $wpdb->get_var("SELECT @@innodb_default_row_format;"); if (in_array(strtolower($rowFormat), ['redundant', 'compact'])) { diff --git a/classes/Tools/Video/Driver/Mux/Elementor/MuxVideoWidget.php b/classes/Tools/Video/Driver/Mux/Elementor/MuxVideoWidget.php deleted file mode 100644 index 7a4d0dbc..00000000 --- a/classes/Tools/Video/Driver/Mux/Elementor/MuxVideoWidget.php +++ /dev/null @@ -1,203 +0,0 @@ -start_controls_section( 'content_section', [ - 'label' => 'Content', - 'tab' => Controls_Manager::TAB_CONTENT, - ] ); - $this->add_control( 'video', [ - 'label' => 'Video', - 'media_type' => 'video', - 'type' => Controls_Manager::MEDIA, - ] ); - $this->add_control( 'poster', [ - 'label' => 'Poster Image', - 'media_type' => 'image', - 'separator' => 'before', - 'type' => Controls_Manager::MEDIA, - ] ); - $this->add_control( 'autoplay', [ - 'label' => 'Auto Play', - 'type' => Controls_Manager::SWITCHER, - 'separator' => 'before', - ] ); - $this->add_control( 'loop', [ - 'label' => 'Loop', - 'type' => Controls_Manager::SWITCHER, - ] ); - $this->add_control( 'muted', [ - 'label' => 'Muted', - 'type' => Controls_Manager::SWITCHER, - ] ); - $this->add_control( 'playsinline', [ - 'label' => 'Play Inline', - 'type' => Controls_Manager::SWITCHER, - 'default' => 'yes', - ] ); - $this->add_control( 'controls', [ - 'label' => 'Show Controls', - 'type' => Controls_Manager::SWITCHER, - 'default' => 'yes', - ] ); - $this->add_control( 'preload', [ - 'label' => 'Preload', - 'type' => Controls_Manager::SELECT, - 'options' => [ - 'auto' => 'Auto', - 'metadata' => 'Metadata', - 'none' => 'None', - ], - 'default' => 'metadata', - ] ); - $this->end_controls_section(); - } - - private function renderEmpty( $message, $hasError = false ) - { - $classes = ( $hasError ? 'has-error' : '' ); - echo <<
{$message}
-RENDER - ; - } - - protected function render() - { - $settings = $this->get_settings_for_display(); - - if ( empty($settings['video']['id']) ) { - $this->renderEmpty( "Please select a video." ); - return; - } - - $asset = MuxAsset::assetForAttachment( $settings['video']['id'] ); - - if ( empty($asset) ) { - $this->renderEmpty( "Please select a Mux video.", true ); - return; - } - - $classes = 'mux-player elementor-mux-player'; - $extras = "data-mux-id='{$asset->muxId}'"; - $metadata = []; - $metadataKey = sanitize_title( gen_uuid( 12 ) ); - $muxSettings = MuxToolSettings::instance(); - $metadataHTML = ''; - $url = $asset->videoUrl(); - - if ( empty($settings['poster']['id']) ) { - $posterUrl = get_the_post_thumbnail_url( $asset->id(), 'large' ); - } else { - $posterUrl = wp_get_attachment_image_url( $settings['poster']['id'], 'large' ); - } - - if ( !empty($posterUrl) ) { - $extras .= "poster = '{$posterUrl}'"; - } - if ( arrayPath( $settings, 'autoplay', 'yes' ) === 'yes' ) { - $extras .= ' autoplay'; - } - if ( arrayPath( $settings, 'loop', 'yes' ) === 'yes' ) { - $extras .= ' loop'; - } - if ( arrayPath( $settings, 'muted', 'yes' ) === 'yes' ) { - $extras .= ' muted'; - } - if ( arrayPath( $settings, 'controls', 'yes' ) === 'yes' ) { - $extras .= ' controls'; - } - if ( arrayPath( $settings, 'playsinline', 'yes' ) === 'yes' ) { - $extras .= ' playsinline'; - } - $preload = arrayPath( $settings, 'preload', 'metadata' ); - $extras .= " preload='{$preload}'"; - $aspect = generateAspectRatio( $asset->width, $asset->height ); - $aspectClass = 'mux-ele-video-container aspect-' . implode( '-', $aspect ); - if ( !empty($muxSettings->playerCSSClasses) ) { - $classes .= " {$muxSettings->playerCSSClasses}"; - } - $sources = ""; - echo << -\t\t\t -\t\t\t{$metadataHTML} -\t\t -RENDER - ; - } - - public static function filterContent( $content ) - { - $vidregex = '/<\\s*figure\\s+class=\\"\\s*mux-ele-video-container(?:[^>]+)>\\s*(]+>)\\s*(]+>)/ms'; - if ( preg_match_all( - $vidregex, - $content, - $matches, - PREG_SET_ORDER, - 0 - ) ) { - foreach ( $matches as $match ) { - $video = $match[1]; - $source = $match[2]; - - if ( preg_match_all( '/data-mux-id\\s*=\\s*(?:\'|")([^\'"]+)/ms', $video, $idMatches ) ) { - $assetId = $idMatches[1][0]; - $asset = MuxAsset::asset( $assetId ); - - if ( !empty($asset) ) { - $newUrl = $asset->videoUrl(); - $content = str_replace( $source, "", $content ); - } - - } - - } - } - return $content; - } - -} \ No newline at end of file diff --git a/classes/Tools/Video/Driver/Mux/Models/MuxAsset.php b/classes/Tools/Video/Driver/Mux/Models/MuxAsset.php index 275174d2..8fd5df3b 100644 --- a/classes/Tools/Video/Driver/Mux/Models/MuxAsset.php +++ b/classes/Tools/Video/Driver/Mux/Models/MuxAsset.php @@ -13,13 +13,24 @@ namespace MediaCloud\Plugin\Tools\Video\Driver\Mux\Models; use MediaCloud\Plugin\Model\Model ; +use MediaCloud\Plugin\Tasks\TaskSchedule ; +use MediaCloud\Plugin\Tools\Storage\StorageConstants ; +use MediaCloud\Plugin\Tools\Storage\StorageTool ; +use MediaCloud\Plugin\Tools\Storage\StorageToolSettings ; +use MediaCloud\Plugin\Tools\ToolsManager ; use MediaCloud\Plugin\Tools\Video\Driver\Mux\MuxAPI ; use MediaCloud\Plugin\Tools\Video\Driver\Mux\MuxToolProSettings ; use MediaCloud\Plugin\Tools\Video\Driver\Mux\MuxToolSettings ; use MediaCloud\Plugin\Utilities\Logging\Logger ; +use MediaCloud\Plugin\Utilities\Prefixer ; use MediaCloud\Vendor\MuxPhp\Models\CreateTrackRequest ; use MediaCloud\Vendor\MuxPhp\Models\CreateTrackResponse ; use function MediaCloud\Plugin\Utilities\arrayPath ; +use MediaCloud\Vendor\Chrisyue\PhpM3u8\Facade\ParserFacade ; +use MediaCloud\Vendor\Chrisyue\PhpM3u8\Stream\TextStream ; +use function MediaCloud\Plugin\Utilities\gen_uuid ; +use function MediaCloud\Plugin\Utilities\ilab_set_time_limit ; +use function MediaCloud\Plugin\Utilities\ilab_stream_download ; /** * Class MuxAsset * @package MediaCloud\Plugin\Tools\Video\Driver\Mux\Models @@ -42,6 +53,10 @@ * @property-read string|null $filmstripUrl * @property-read array|null $muxMetadata * @property-read array|null $subtitles + * @property int $isTransferred + * @property int $isDeleted + * @property mixed|null $transferData + * @property mixed|null $relatedFiles */ class MuxAsset extends Model { @@ -100,6 +115,16 @@ class MuxAsset extends Model * @var int */ protected $height = 0 ; + /** + * Has the video been transferred to local or cloud storage? + * @var int + */ + protected $isTransferred = 0 ; + /** + * Has the video been deleted from Mux? + * @var int + */ + protected $isDeleted = 0 ; /** * Aspect ratio * @var null @@ -110,27 +135,243 @@ class MuxAsset extends Model * @var null|mixed */ protected $jsonData = null ; + /** + * Mux JSON data + * @var null|mixed + */ + protected $relatedFiles = null ; + /** + * Mux transfer data + * @var null|mixed + */ + protected $transferData = null ; /** @var bool|MuxPlaybackID[] */ protected $_playbackIds = false ; /** @var array[] */ protected $_subtitles = false ; protected $modelProperties = array( - 'muxId' => '%s', - 'status' => '%s', - 'createdAt' => '%d', - 'title' => '%s', - 'attachmentId' => '%d', - 'duration' => '%f', - 'frameRate' => '%f', - 'mp4Support' => '%d', - 'width' => '%d', - 'height' => '%d', - 'aspectRatio' => '%s', - 'jsonData' => '%s', + 'muxId' => '%s', + 'status' => '%s', + 'createdAt' => '%d', + 'title' => '%s', + 'attachmentId' => '%d', + 'duration' => '%f', + 'frameRate' => '%f', + 'mp4Support' => '%d', + 'width' => '%d', + 'height' => '%d', + 'aspectRatio' => '%s', + 'jsonData' => '%s', + 'transferData' => '%s', + 'isTransferred' => '%d', + 'isDeleted' => '%d', + 'relatedFiles' => '%s', ) ; - protected $jsonProperties = array( 'jsonData' ) ; + protected $jsonProperties = array( 'jsonData', 'transferData', 'relatedFiles' ) ; //endregion //region Static + public static $subtitleLanguages = array( + 'af' => 'Afrikaans', + 'af-ZA' => 'Afrikaans - South Africa', + 'ar' => 'Arabic', + 'ar-AE' => 'Arabic - United Arab Emirates', + 'ar-BH' => 'Arabic - Bahrain', + 'ar-DZ' => 'Arabic - Algeria', + 'ar-EG' => 'Arabic - Egypt', + 'ar-IQ' => 'Arabic - Iraq', + 'ar-JO' => 'Arabic - Jordan', + 'ar-KW' => 'Arabic - Kuwait', + 'ar-LB' => 'Arabic - Lebanon', + 'ar-LY' => 'Arabic - Libya', + 'ar-MA' => 'Arabic - Morocco', + 'ar-OM' => 'Arabic - Oman', + 'ar-QA' => 'Arabic - Qatar', + 'ar-SA' => 'Arabic - Saudi Arabia', + 'ar-SY' => 'Arabic - Syria', + 'ar-TN' => 'Arabic - Tunisia', + 'ar-YE' => 'Arabic - Yemen', + 'az' => 'Azeri', + 'az-AZ' => 'Cyrl Azeri (Cyrillic) - Azerbaijan', + 'az-AZ-Latn' => 'Azeri (Latin) - Azerbaijan', + 'be' => 'Belarusian', + 'be-BY' => 'Belarusian - Belarus', + 'bg' => 'Bulgarian', + 'bg-BG' => 'Bulgarian - Bulgaria', + 'ca' => 'Catalan', + 'ca-ES' => 'Catalan - Catalan', + 'cs' => 'Czech', + 'cs-CZ' => 'Czech - Czech Republic', + 'da' => 'Danish', + 'da-DK' => 'Danish - Denmark', + 'de' => 'German', + 'de-AT' => 'German - Austria', + 'de-CH' => 'German - Switzerland', + 'de-DE' => 'German - Germany', + 'de-LI' => 'German - Liechtenstein', + 'de-LU' => 'German - Luxembourg', + 'div' => 'Dhivehi', + 'div-MV' => 'Dhivehi - Maldives', + 'el' => 'Greek', + 'el-GR' => 'Greek - Greece', + 'en' => 'English', + 'en-AU' => 'English - Australia', + 'en-BZ' => 'English - Belize', + 'en-CA' => 'English - Canada', + 'en-CB' => 'English - Caribbean', + 'en-GB' => 'English - United Kingdom', + 'en-IE' => 'English - Ireland', + 'en-JM' => 'English - Jamaica', + 'en-NZ' => 'English - New Zealand', + 'en-PH' => 'English - Philippines', + 'en-TT' => 'English - Trinidad and Tobago', + 'en-US' => 'English - United States', + 'en-ZA' => 'English - South Africa', + 'en-ZW' => 'English - Zimbabwe', + 'es' => 'Spanish', + 'es-AR' => 'Spanish - Argentina', + 'es-BO' => 'Spanish - Bolivia', + 'es-CLe' => 'Spanish - Chile', + 'es-CO' => 'Spanish - Colombia', + 'es-CR' => 'Spanish - Costa Rica', + 'es-DO' => 'Spanish - Dominican Republic', + 'es-EC' => 'Spanish - Ecuador', + 'es-ES' => 'Spanish - Spain', + 'es-GT' => 'Spanish - Guatemala', + 'es-HN' => 'Spanish - Honduras', + 'es-MX' => 'Spanish - Mexico', + 'es-NI' => 'Spanish - Nicaragua', + 'es-PA' => 'Spanish - Panama', + 'es-PE' => 'Spanish - Peru', + 'es-PR' => 'Spanish - Puerto Rico', + 'es-PY' => 'Spanish - Paraguay', + 'es-SV' => 'Spanish - El Salvador', + 'es-UY' => 'Spanish - Uruguay', + 'es-VE' => 'Spanish - Venezuela', + 'et' => 'Estonian', + 'et-EE' => 'Estonian - Estonia', + 'eu' => 'Basque', + 'eu-ES' => 'Basque - Basque', + 'fa' => 'Farsi', + 'fa-IR' => 'Farsi - Iran', + 'fi' => 'Finnish', + 'fi-FI' => 'Finnish - Finland', + 'fo' => 'Faroese', + 'fo-FO' => 'Faroese - Faroe Islands', + 'fr' => 'French', + 'fr-BE' => 'French - Belgium', + 'fr-CA' => 'French - Canada', + 'fr-CH' => 'French - Switzerland', + 'fr-FR' => 'French - France', + 'fr-LU' => 'French - Luxembourg', + 'fr-MC' => 'French - Monaco', + 'gl' => 'Galician', + 'gl-ES' => 'Galician - Galician', + 'gu' => 'Gujarati', + 'gu-IN' => 'Gujarati - India', + 'he' => 'Hebrew', + 'he-IL' => 'Hebrew - Israel', + 'hi' => 'Hindi', + 'hi-IN' => 'Hindi - India', + 'hr' => 'Croatian', + 'hr-HR' => 'Croatian - Croatia', + 'hu' => 'Hungarian', + 'hu-HU' => 'Hungarian - Hungary', + 'hy' => 'Armenian', + 'hy-AM' => 'Armenian - Armenia', + 'id' => 'Indonesian', + 'id-ID' => 'Indonesian - Indonesia', + 'is' => 'Icelandic', + 'is-IS' => 'Icelandic - Iceland', + 'it' => 'Italian', + 'it-CH' => 'Italian Italian - Switzerland', + 'it-IT' => 'Italian - Italy', + 'ja' => 'Japanese', + 'ja-JP' => 'Japanese - Japan', + 'ka' => 'Georgian', + 'ka-GE' => 'Georgian - Georgia', + 'kk' => 'Kazakh', + 'kk-KZ' => 'Kazakh - Kazakhstan', + 'kn' => 'Kannada', + 'kn-IN' => 'Kannada - India', + 'ko' => 'Korean', + 'kok' => 'Konkani', + 'kok-IN' => 'Konkani - India', + 'ko-KR' => 'Korean - Korea', + 'ky' => 'Kyrgyz', + 'ky-KG' => 'Kyrgyz - Kyrgyzstan', + 'lt' => 'Lithuanian', + 'lt-LT' => 'Lithuanian - Lithuania', + 'lv' => 'Latvian', + 'lv-LV' => 'Latvian - Latvia', + 'mk' => 'Macedonian', + 'mk-MK' => 'Macedonian - Former Yugoslav Republic of Macedonia', + 'mn' => 'Mongolian', + 'mn-MN' => 'Mongolian - Mongolia', + 'mr' => 'Marathi', + 'mr-IN' => 'Marathi - India', + 'ms' => 'Malay', + 'ms-BN' => 'Malay - Brunei', + 'ms-MY' => 'Malay - Malaysia', + 'nb-NO' => 'Norwegian (Bokm?l) - Norway', + 'nl' => 'Dutch', + 'nl-BE' => 'Dutch - Belgium', + 'nl-NL' => 'Dutch - The Netherlands', + 'nn-NO' => 'Norwegian (Nynorsk) - Norway', + 'no' => 'Norwegian', + 'pa' => 'Punjabi', + 'pa-IN' => 'Punjabi - India', + 'pl' => 'Polish', + 'pl-PL' => 'Polish - Poland', + 'pt' => 'Portuguese', + 'pt-BR' => 'Portuguese - Brazil', + 'pt-PT' => 'Portuguese - Portugal', + 'ro' => 'Romanian', + 'ro-RO' => 'Romanian - Romania', + 'ru' => 'Russian', + 'ru-RU' => 'Russian - Russia', + 'sa' => 'Sanskrit', + 'sa-IN' => 'Sanskrit - India', + 'sk' => 'Slovak', + 'sk-SK' => 'Slovak - Slovakia', + 'sl' => 'Slovenian', + 'sl-SI' => 'Slovenian - Slovenia', + 'sq' => 'Albanian', + 'sq-AL' => 'Albanian - Albania', + 'sr-SP-Cyrl' => 'Serbian (Cyrillic) - Serbia', + 'sr-SP-Latn' => 'Serbian (Latin) - Serbia', + 'sv' => 'Swedish', + 'sv-FI' => 'Swedish - Finland', + 'sv-SE' => 'Swedish - Sweden', + 'sw' => 'Swahili', + 'sw-KE' => 'Swahili - Kenya', + 'syr' => 'Syriac', + 'syr-SY' => 'Syriac - Syria', + 'ta' => 'Tamil', + 'ta-IN' => 'Tamil - India', + 'te' => 'Telugu', + 'te-IN' => 'Telugu - India', + 'th' => 'Thai', + 'th-TH' => 'Thai - Thailand', + 'tr' => 'Turkish', + 'tr-TR' => 'Turkish - Turkey', + 'tt' => 'Tatar', + 'tt-RU' => 'Tatar - Russia', + 'uk' => 'Ukrainian', + 'uk-UA' => 'Ukrainian - Ukraine', + 'ur' => 'Urdu', + 'ur-PK' => 'Urdu - Pakistan', + 'uz' => 'Uzbek', + 'uz-UZ-Cyrl' => 'Uzbek (Cyrillic) - Uzbekistan', + 'uz-UZ-Latn' => 'Uzbek (Latin) - Uzbekistan', + 'vi' => 'Vietnamese', + 'zh-CHT' => 'Chinese (Traditional)', + 'zh-CHS' => 'Chinese (Simplified)', + 'zh-CN' => 'Chinese - China', + 'zh-HK' => 'Chinese - Hong Kong SAR', + 'zh-MO' => 'Chinese - Macao SAR', + 'zh-SG' => 'Chinese - Singapore', + 'zh-TW' => 'Chinese - Taiwan', + ) ; public static function table() { global $wpdb ; @@ -216,15 +457,23 @@ public function __get( $name ) } $this->_subtitles = []; $tracks = arrayPath( $this->jsonData, 'tracks', [] ); - if ( empty($tracks) ) { - return []; - } - foreach ( $tracks as $track ) { - $type = arrayPath( $track, 'text_type', null ); - if ( !empty($type) && $type === 'subtitles' ) { - $this->_subtitles[] = $track; + + if ( !empty($tracks) ) { + foreach ( $tracks as $track ) { + $type = arrayPath( $track, 'text_type', null ); + if ( !empty($type) && $type === 'subtitles' ) { + $this->_subtitles[] = $track; + } + } + foreach ( $this->_subtitles as &$subtitle ) { + $subtitle['local'] = false; } } + + $captions = get_post_meta( $this->attachmentId, '_captions', true ); + if ( $captions && is_array( $captions ) ) { + $this->_subtitles = array_merge( $this->_subtitles, $captions ); + } return $this->_subtitles; } @@ -275,8 +524,32 @@ public function willDelete() //endregion //region Captions + public function addLocalCaptions( $language, $captionsURL, $closedCaptions = false ) + { + $captions = get_post_meta( $this->attachmentId, '_captions', true ); + if ( !is_array( $captions ) ) { + $captions = []; + } + if ( !isset( static::$subtitleLanguages[$language] ) ) { + return false; + } + $captions[] = [ + 'id' => gen_uuid(), + 'name' => static::$subtitleLanguages[$language], + 'language_code' => $language, + 'local' => true, + 'cc' => $closedCaptions, + 'url' => $captionsURL, + ]; + update_post_meta( $this->attachmentId, '_captions', $captions ); + return true; + } + public function addCaptions( $language, $captionsURL, $closedCaptions = false ) { + if ( $this->isTransferred ) { + return $this->addLocalCaptions( $language, $captionsURL, $closedCaptions ); + } $req = new CreateTrackRequest( [ 'url' => $captionsURL, 'type' => 'text', @@ -301,6 +574,20 @@ public function addCaptions( $language, $captionsURL, $closedCaptions = false ) public function deleteCaptions( $captionsId ) { + + if ( $this->isTransferred ) { + $captions = get_post_meta( $this->attachmentId, '_captions', true ) ?? []; + $newCaptions = []; + foreach ( $captions as $caption ) { + if ( $caption['id'] == $captionsId ) { + continue; + } + $newCaptions[] = $caption; + } + update_post_meta( $this->attachmentId, '_captions', $newCaptions ); + return true; + } + try { Logger::info( "Deleting track {$captionsId} for asset {$this->muxId}", @@ -345,21 +632,53 @@ public function publicVideoUrl() public function videoUrl( $preferSecure = true ) { - $url = ( $preferSecure ? $this->secureVideoUrl() : $this->publicVideoUrl() ); - if ( !empty($url) ) { - return $url; + + if ( $this->isTransferred && !empty($this->transferData) ) { + + if ( $this->transferData['source'] === 's3' ) { + /** @var StorageTool $storageTool */ + $storageTool = ToolsManager::instance()->tools['storage']; + return $storageTool->getAttachmentURLFromMeta( [ + 'type' => 'application/vnd.apple.mpegurl', + 's3' => [ + 'key' => $this->transferData['playlist'], + ], + ] ); + } else { + $uploadDir = wp_upload_dir(); + return trailingslashit( $uploadDir['baseurl'] ) . $this->transferData['playlist']; + } + + } else { + $url = ( $preferSecure ? $this->secureVideoUrl() : $this->publicVideoUrl() ); + if ( !empty($url) ) { + return $url; + } + return ( $preferSecure ? $this->publicVideoUrl() : $this->secureVideoUrl() ); } - return ( $preferSecure ? $this->publicVideoUrl() : $this->secureVideoUrl() ); + } public function secureRenditionUrl( $qualityLevel ) { - return null; + Logger::info( + "Getting secure rendition url for {$qualityLevel}", + [], + __METHOD__, + __LINE__ + ); + return $this->publicRenditionUrl( $qualityLevel ); } public function publicRenditionUrl( $qualityLevel ) { $pid = $this->__get( 'publicPlaybackID' ); + Logger::info( + "Getting public rendition URL for {$pid} at {$qualityLevel}", + [], + __METHOD__, + __LINE__ + ); if ( empty($pid) ) { return null; } @@ -368,11 +687,57 @@ public function publicRenditionUrl( $qualityLevel ) public function renditionUrl( $qualityLevel, $preferSecure = true ) { + + if ( $this->isTransferred && !empty($this->transferData) ) { + if ( isset( $this->transferData['renditions'][$qualityLevel] ) ) { + + if ( $this->transferData['source'] === 's3' ) { + /** @var StorageTool $storageTool */ + $storageTool = ToolsManager::instance()->tools['storage']; + return $storageTool->getAttachmentURLFromMeta( [ + 'type' => 'video/mp4', + 's3' => [ + 'key' => $this->transferData['renditions'][$qualityLevel], + ], + ] ); + } else { + $uploadDir = wp_upload_dir(); + return trailingslashit( $uploadDir['baseurl'] ) . $this->transferData['renditions'][$qualityLevel]; + } + + } + + if ( $qualityLevel === 'high.mp4' ) { + return $this->renditionUrl( 'medium.mp4', $preferSecure ); + } else { + if ( $qualityLevel === 'medium.mp4' ) { + return $this->renditionUrl( 'low.mp4', $preferSecure ); + } + } + + return null; + } + $url = ( $preferSecure ? $this->secureRenditionUrl( $qualityLevel ) : $this->publicRenditionUrl( $qualityLevel ) ); + Logger::info( + "Rendition URL: {$qualityLevel} => {$url}", + [], + __METHOD__, + __LINE__ + ); if ( !empty($url) ) { return $url; } - return ( $preferSecure ? $this->publicRenditionUrl( $qualityLevel ) : $this->secureRenditionUrl( $qualityLevel ) ); + + if ( $qualityLevel === 'high.mp4' ) { + return $this->renditionUrl( 'medium.mp4', $preferSecure ); + } else { + if ( $qualityLevel === 'medium.mp4' ) { + return $this->renditionUrl( 'low.mp4', $preferSecure ); + } + } + + return null; } //endregion @@ -473,6 +838,29 @@ public function gifUrl( $preferSecure = true ) return ( $preferSecure ? $this->publicGIFUrl() : $this->secureGIFUrl() ); } + //endregion + //region Filmstrips + /** + * Generates filmstrip for video + * + * @throws \Freemius_Exception + */ + public function generateFilmstrip() + { + Logger::info( + 'Mux: generateFilmstripForAttachment', + [], + __METHOD__, + __LINE__ + ); + Logger::warning( + 'Mux: generateFilmstripForAttachment could not be run, not premium', + [], + __METHOD__, + __LINE__ + ); + } + //endregion //region Queries /** @@ -531,5 +919,147 @@ public static function findOrCreate( $muxId ) $asset->muxId = $muxId; return $asset; } + + //endregion + //region Transfer + private function downloadFile( + string $sourceUrl, + string $destKey, + string $destFile, + string $mimeType, + StorageTool $storageTool, + bool $localOnly, + callable $status, + callable $error + ) + { + if ( file_exists( $destFile ) ) { + @unlink( $destFile ); + } + $status( + "Downloading {$sourceUrl} ... ", + false, + __METHOD__, + __LINE__ + ); + $result = ilab_stream_download( $sourceUrl, $destFile ); + $status( + "Done.", + true, + __METHOD__, + __LINE__ + ); + + if ( !$result ) { + $error( "Error downloading {$sourceUrl}", __METHOD__, __LINE__ ); + return false; + } + + + if ( !$localOnly ) { + $status( + "Uploading {$destKey} ... ", + false, + __METHOD__, + __LINE__ + ); + $storageTool->client()->upload( + $destKey, + $destFile, + StorageConstants::ACL_PUBLIC_READ, + null, + null, + $mimeType + ); + $status( + "Done.", + true, + __METHOD__, + __LINE__ + ); + + if ( StorageToolSettings::deleteOnUpload() ) { + $status( + "Deleting {$destFile} from local ... ", + false, + __METHOD__, + __LINE__ + ); + @unlink( $destFile ); + $status( + "Done.", + true, + __METHOD__, + __LINE__ + ); + } + + } + + return true; + } + + /** + * Transfers a Mux video to cloud storage and/or local storage + * + * @param string $dest + * + * @return string[]|void + */ + public function getFilesToTransfer( string $dest ) + { + + if ( $this->isDeleted === 1 ) { + Logger::info( + "Skipping deleted asset.", + true, + __METHOD__, + __LINE__ + ); + return []; + } + + $allFiles = []; + return $allFiles; + } + + /** + * Transfers a Mux video to cloud storage and/or local storage + * + * @param string $dest + * @param bool $delete + * @param bool $localOnly + * @param callable $status + * @param callable $error + * + * @return false|void + * @throws \MediaCloud\Plugin\Tools\Storage\StorageException + */ + public function transfer( + string $dest, + bool $delete, + bool $localOnly, + callable $status, + callable $error + ) + { + + if ( $this->isDeleted === 1 ) { + $status( + "Skipping deleted asset.", + true, + __METHOD__, + __LINE__ + ); + return true; + } + + $relatedFiles = []; + return true; + } + + public function backgroundTransfer() + { + } } \ No newline at end of file diff --git a/classes/Tools/Video/Driver/Mux/MuxHooks.php b/classes/Tools/Video/Driver/Mux/MuxHooks.php index 9656cd67..4b7a224f 100644 --- a/classes/Tools/Video/Driver/Mux/MuxHooks.php +++ b/classes/Tools/Video/Driver/Mux/MuxHooks.php @@ -25,6 +25,7 @@ use MediaCloud\Vendor\MuxPhp\Models\PlaybackPolicy ; use function MediaCloud\Plugin\Utilities\arrayPath ; use function MediaCloud\Plugin\Utilities\gen_uuid ; +use function MediaCloud\Plugin\Utilities\postIdExists ; class MuxHooks { /** @var MuxToolSettings|MuxToolProSettings */ @@ -59,6 +60,9 @@ public function __construct() ); } + if ( is_admin() ) { + add_action( 'wp_ajax_mcloud_replace_poster', [ $this, 'ajaxReplacePoster' ] ); + } add_filter( 'template_include', [ $this, 'handleWebhook' ] ); } @@ -255,7 +259,7 @@ protected function assignThumbnailForAsset( $asset ) __METHOD__, __LINE__ ); - $this->generateFilmstripForAttachment( $asset ); + $asset->generateFilmstrip(); return; } @@ -312,28 +316,7 @@ protected function assignThumbnailForAsset( $asset ) update_post_meta( $thumbId, '_wp_attached_file', $thumbAttachmentMeta['file'] ); update_post_meta( $asset->attachmentId, '_thumbnail_id', $thumbId ); wp_update_attachment_metadata( $thumbId, $thumbAttachmentMeta ); - $this->generateFilmstripForAttachment( $asset ); - } - - /** - * @param MuxAsset $asset - * - * @throws \Freemius_Exception - */ - protected function generateFilmstripForAttachment( $asset ) - { - Logger::info( - 'Mux: generateFilmstripForAttachment', - [], - __METHOD__, - __LINE__ - ); - Logger::warning( - 'Mux: generateFilmstripForAttachment could not be run, not premium', - [], - __METHOD__, - __LINE__ - ); + $asset->generateFilmstrip(); } public function handleStaticRenditionsReady( $jsonData ) @@ -384,6 +367,7 @@ public function handleStaticRenditionsReady( $jsonData ) } } + $asset->backgroundTransfer(); } public function handleAssetReady( $jsonData ) @@ -496,6 +480,13 @@ public function handleAssetDeleted( $jsonData ) if ( empty($asset) ) { return; } + + if ( $asset->isTransferred == 1 ) { + $asset->isDeleted = 1; + $asset->save(); + return; + } + $asset->delete(); } @@ -711,5 +702,28 @@ public function handleUpload( $attachmentId, $file, $meta ) { $this->handleDirectUpload( $attachmentId, $meta ); } + + public function ajaxReplacePoster() + { + wp_verify_nonce( $_POST['nonce'], 'media-cloud-mux-replace-poster' ); + $attachmentId = arrayPath( $_POST, 'attachmentId', null ); + if ( empty($attachmentId) ) { + wp_send_json_error( 'Missing attachment ID', 400 ); + } + $newPosterId = arrayPath( $_POST, 'posterId', null ); + if ( empty($newPosterId) ) { + wp_send_json_error( 'Missing poster ID', 400 ); + } + if ( !postIdExists( $attachmentId ) ) { + wp_send_json_error( 'Invalid attachment ID', 400 ); + } + if ( !postIdExists( $newPosterId ) ) { + wp_send_json_error( 'Invalid poster ID', 400 ); + } + update_post_meta( $attachmentId, '_thumbnail_id', $newPosterId ); + wp_send_json( [ + 'success' => true, + ] ); + } } \ No newline at end of file diff --git a/classes/Tools/Video/Driver/Mux/MuxTool.php b/classes/Tools/Video/Driver/Mux/MuxTool.php index 981bf5fa..acd7a8a6 100644 --- a/classes/Tools/Video/Driver/Mux/MuxTool.php +++ b/classes/Tools/Video/Driver/Mux/MuxTool.php @@ -13,6 +13,7 @@ namespace MediaCloud\Plugin\Tools\Video\Driver\Mux; use MediaCloud\Plugin\Tasks\TaskManager ; +use MediaCloud\Plugin\Tools\Storage\StorageTool ; use MediaCloud\Plugin\Tools\Video\Driver\Mux\Data\MuxDatabase ; use MediaCloud\Plugin\Tools\Video\Driver\Mux\Models\MuxAsset ; use MediaCloud\Plugin\Tools\Video\Driver\Mux\Tasks\MigrateToMuxTask ; @@ -23,12 +24,15 @@ use MediaCloud\Plugin\Utilities\View ; use function MediaCloud\Plugin\Utilities\anyEmpty ; use function MediaCloud\Plugin\Utilities\arrayPath ; +use function MediaCloud\Plugin\Utilities\ilab_set_time_limit ; class MuxTool extends Tool { /** @var null|MuxToolSettings|MuxToolProSettings */ protected $settings = null ; /** @var MuxHooks */ protected $hooks = null ; + private $muxIconSVG = null ; + private $hlsIconSVG = null ; public function __construct( $toolName, $toolInfo, $toolManager ) { $this->settings = MuxToolSettings::instance(); @@ -80,7 +84,11 @@ public function setup() $this->integrateWithMediaLibrary(); $this->integrateWithAdmin(); } - + + $this->integrateREST(); + if ( $this->settings->deleteFromMux ) { + add_action( 'delete_attachment', [ $this, 'deleteAttachment' ], 999 ); + } } } @@ -201,6 +209,7 @@ protected function actionCaptionUpload() 'message' => 'Missing asset ID.', ], 400 ); } + /** @var MuxAsset $asset */ $asset = MuxAsset::instance( $aid ); if ( empty($asset) ) { wp_send_json( [ @@ -216,12 +225,13 @@ protected function actionCaptionUpload() ], 400 ); } $cc = !empty((int) arrayPath( $_POST, 'cc', null )); + $allowedMimes = ( $asset->isTransferred ? [ 'text/plain', 'text/vtt' ] : [ 'text/vtt', 'text/plain', 'text/srt' ] ); $finfo = new \finfo( FILEINFO_MIME ); $info = $finfo->file( $_FILES['file']['tmp_name'] ); $infoParts = explode( ';', $info ); $mimeType = array_shift( $infoParts ); - if ( !in_array( $mimeType, [ 'text/plain', ' text/vtt', 'text/srt' ] ) ) { + if ( !in_array( $mimeType, $allowedMimes ) ) { Logger::error( "Invalid captions mime type: {$mimeType}", [], @@ -230,7 +240,7 @@ protected function actionCaptionUpload() ); wp_send_json( [ 'status' => 'error', - 'message' => 'Invalid file type', + 'message' => "Invalid file type {$mimeType}", ], 400 ); } @@ -301,20 +311,70 @@ protected function integrateWithMediaLibrary() if ( ToolsManager::instance()->toolEnabled( 'storage' ) ) { add_filter( 'media-cloud/media-library/attachment-classes', function ( $additionalClasses ) { + $additionalClasses = '<# if (data.hasOwnProperty("hls")) {#>has-hls<#}#>' . $additionalClasses; $additionalClasses = '<# if (data.hasOwnProperty("mux")) {#>has-mux mux-status-{{data.mux.status}}<#}#>' . $additionalClasses; return $additionalClasses; } ); add_filter( 'media-cloud/media-library/attachment-icons', function ( $additionalIcons ) { $muxIcon = ''; - return $muxIcon . $additionalIcons; + $hlsIcon = ''; + return $hlsIcon . $muxIcon . $additionalIcons; } ); } else { $this->hookMediaLibraryGrid(); } - if ( $this->settings->deleteFromMux ) { - add_action( 'delete_attachment', [ $this, 'deleteAttachment' ], 999 ); + add_filter( 'manage_media_columns', function ( $cols ) { + $cols["cloud"] = 'Cloud'; + return $cols; + } ); + add_action( + 'manage_media_custom_column', + function ( $column_name, $id ) { + $asset = MuxAsset::assetForAttachment( $id ); + if ( !$asset ) { + return; + } + + if ( $column_name == "cloud" ) { + $muxIcon = $this->muxIcon(); + $hlsIcon = $this->hlsIcon(); + + if ( !$asset->isTransferred ) { + echo "" ; + } else { + echo "" ; + } + + } + + }, + PHP_INT_MAX - 8, + 2 + ); + $this->hookMediaDetails(); + } + + private function muxIcon() + { + + if ( $this->muxIconSVG === null ) { + $svg = file_get_contents( ILAB_PUB_IMG_DIR . '/logo-mux-white.svg' ); + $this->muxIconSVG = 'data:image/svg+xml;base64,' . base64_encode( $svg ); + } + + return $this->muxIconSVG; + } + + private function hlsIcon() + { + + if ( $this->hlsIconSVG === null ) { + $svg = file_get_contents( ILAB_PUB_IMG_DIR . '/logo-hls.svg' ); + $this->hlsIconSVG = 'data:image/svg+xml;base64,' . base64_encode( $svg ); } + + return $this->hlsIconSVG; } private function hookMediaLibraryGrid() @@ -348,6 +408,68 @@ private function hookMediaLibraryGrid() } ); } + private function hookMediaDetails() + { + add_filter( + 'media_row_actions', + function ( $actions, $post ) { + + if ( strpos( $post->post_mime_type, 'video' ) === 0 ) { + $nonce = wp_create_nonce( 'media-cloud-mux-replace-poster' ); + $newaction['mcloud_mux_replace_poster'] = '' . __( 'Replace Poster' ) . ''; + return array_merge( $actions, $newaction ); + } + + return $actions; + }, + 10, + 2 + ); + add_filter( 'mediacloud/ui/media-detail-buttons', function ( $buttons ) { + $buttons[] = [ + 'type' => 'video', + 'class' => 'replace-video-poster', + 'thickbox' => false, + 'data' => [ + 'nonce' => wp_create_nonce( 'media-cloud-mux-replace-poster' ), + 'attachment-id' => '{{data.id}}', + ], + 'label' => __( 'Replace Poster' ), + 'url' => '#', + ]; + return $buttons; + }, 2 ); + add_filter( 'mediacloud/ui/media-detail-links', function ( $links ) { + $links[] = [ + 'type' => 'video', + 'class' => 'replace-video-poster', + 'thickbox' => false, + 'label' => __( 'Replace Poster' ), + 'data' => [ + 'nonce' => wp_create_nonce( 'media-cloud-mux-replace-poster' ), + 'attachment-id' => '{{data.id}}', + ], + 'url' => '#', + ]; + return $links; + }, 2 ); + } + + /** + * Generate the url for selecting the poster + * + * @param $id + * @return string + */ + public function posterSelectURL( $id, $partial = false ) + { + $url = parse_url( get_admin_url( null, 'admin-ajax.php' ), PHP_URL_PATH ) . "?action=mcloud_poster_select&post={$id}"; + if ( $partial === true ) { + $url .= '&partial=1'; + } + return $url; + } + /** * Filters the attachment data prepared for JavaScript. (https://core.trac.wordpress.org/browser/tags/4.8/src/wp-includes/media.php#L3279) * @@ -363,9 +485,31 @@ public function prepareAttachmentForJS( $response, $attachment, $meta ) return $response; } $mux = $meta['mux']; + $asset = null; + $muxId = arrayPath( $mux, 'muxId', null ); + if ( $muxId ) { + $asset = MuxAsset::asset( $muxId ); + } + + if ( $asset && ($asset->isTransferred == 1 || $asset->isDeleted == 1) ) { + if ( $asset->isTransferred ) { + $response['hls'] = [ + 'url' => explode( '?', $asset->videoUrl() )[0], + 'subtitles' => $asset->subtitles, + ]; + } + return $response; + } else { + + if ( $asset ) { + $mux['url'] = $asset->videoUrl(); + $mux['subtitles'] = []; + } + + } + if ( !empty($_REQUEST['post_id']) ) { try { - $asset = MuxAsset::asset( $mux['muxId'] ); if ( $asset === null ) { return $response; } @@ -387,20 +531,41 @@ public function prepareAttachmentForJS( $response, $attachment, $meta ) public function deleteAttachment( $id ) { - if ( !$this->settings->deleteFromMux ) { + $asset = MuxAsset::assetForAttachment( $id ); + if ( $asset === null ) { return $id; } - $data = wp_get_attachment_metadata( $id ); - $muxId = arrayPath( $data, 'mux/muxId', null ); - if ( empty($muxId) ) { + if ( !$this->settings->deleteFromMux ) { return $id; } - $asset = MuxAsset::asset( $muxId ); - if ( $asset === null ) { - return $asset; + + if ( $asset->isTransferred && is_array( $asset->relatedFiles ) ) { + ilab_set_time_limit( 0 ); + /** @var StorageTool $storageTool */ + $storageTool = ( ToolsManager::instance()->toolEnabled( 'storage' ) ? ToolsManager::instance()->tools['storage'] : null ); + $uploadDirs = wp_upload_dir(); + foreach ( $asset->relatedFiles as $file ) { + $filepath = trailingslashit( $uploadDirs['basedir'] ) . $file; + if ( file_exists( $filepath ) ) { + @unlink( $filepath ); + } + if ( $asset->transferData && $storageTool && $asset->transferData['source'] === 's3' ) { + try { + $storageTool->client()->delete( $file ); + } catch ( \Exception $ex ) { + Logger::error( + "Mux: Exception deleting file {$file} from S3: " . $ex->getMessage(), + [], + __METHOD__, + __LINE__ + ); + } + } + } } + try { - MuxAPI::assetAPI()->deleteAsset( $muxId ); + MuxAPI::assetAPI()->deleteAsset( $asset->muxId ); } catch ( \Exception $ex ) { Logger::error( 'Mux: Error deleting asset from Mux: ' . $ex->getMessage(), @@ -412,5 +577,34 @@ public function deleteAttachment( $id ) $asset->delete(); return $id; } + + //endregion + //region REST + private function integrateREST() + { + add_action( 'rest_api_init', function () { + register_rest_field( 'attachment', 'hls', [ + 'get_callback' => function ( $data ) { + $attachment = $data['id']; + $asset = MuxAsset::assetForAttachment( $attachment ); + if ( !$asset ) { + return null; + } + return [ + 'playlist' => $asset->videoUrl(), + 'poster' => get_the_post_thumbnail_url( $attachment, 'full' ), + 'filmstrip' => $asset->filmstripUrl, + 'mp4' => $asset->renditionUrl( $this->settings->playerMP4Quality ), + 'subtitles' => $asset->subtitles, + 'width' => (int) $asset->width, + 'height' => (int) $asset->height, + 'duration' => floatval( $asset->duration ), + ]; + }, + 'update_callback' => null, + 'schema' => null, + ] ); + } ); + } } \ No newline at end of file diff --git a/classes/Tools/Video/Player/Elementor/MediaCloudVideoWidget.php b/classes/Tools/Video/Player/Elementor/MediaCloudVideoWidget.php new file mode 100644 index 00000000..6b69d5e7 --- /dev/null +++ b/classes/Tools/Video/Player/Elementor/MediaCloudVideoWidget.php @@ -0,0 +1,214 @@ +start_controls_section('content_section', [ + 'label' => 'Content', + 'tab' => Controls_Manager::TAB_CONTENT + ]); + + $this->add_control('video', [ + 'label' => 'Video', + 'media_type' => 'video', + 'type' => Controls_Manager::MEDIA, + ]); + + $this->add_control('poster', [ + 'label' => 'Poster Image', + 'media_type' => 'image', + 'separator' => 'before', + 'type' => Controls_Manager::MEDIA, + ]); + + + $this->add_control('autoplay', [ + 'label' => 'Auto Play', + 'type' => Controls_Manager::SWITCHER, + 'separator' => 'before', + ]); + + + $this->add_control('loop', [ + 'label' => 'Loop', + 'type' => Controls_Manager::SWITCHER, + ]); + + $this->add_control('muted', [ + 'label' => 'Muted', + 'type' => Controls_Manager::SWITCHER, + ]); + + $this->add_control('playsinline', [ + 'label' => 'Play Inline', + 'type' => Controls_Manager::SWITCHER, + 'default' => 'yes' + ]); + + $this->add_control('controls', [ + 'label' => 'Show Controls', + 'type' => Controls_Manager::SWITCHER, + 'default' => 'yes' + ]); + + $this->add_control('preload', [ + 'label' => 'Preload', + 'type' => Controls_Manager::SELECT, + 'options' => [ + 'auto' => 'Auto', + 'metadata' => 'Metadata', + 'none' => 'None' + ], + 'default' => 'metadata' + ]); + + $this->end_controls_section(); + } + + private function renderEmpty($message, $hasError = false) { + $classes = ($hasError) ? 'has-error' : ''; + + echo <<
{$message}
+RENDER; + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + if (empty($settings['video']['id'])) { + $this->renderEmpty("Please select a video."); + return; + } + + /** @var VideoPlayerTool $playerTool */ + $playerTool = ToolsManager::instance()->tools['video-player']; + + [ + 'classes' => $classes, + 'extras' => $extras, + 'source' => $source, + 'metadataHTML' => $metadataHTML, + 'metadataKey' => $metadataKey, + 'asset' => $asset + ] = $playerTool->videoPlayerProps($settings['video']['id']); + + $classes .= ' elementor-mux-player'; + $extras .= " data-attachment-id='{$settings['video']['id']}'"; + + if (empty($settings['poster']['id'])) { + $posterUrl = get_the_post_thumbnail_url($settings['video']['id'], 'full'); + } else { + $posterUrl = wp_get_attachment_image_url($settings['poster']['id'], 'full'); + } + + if (!empty($posterUrl)) { + $extras .= " poster='{$posterUrl}'"; + } + + if (arrayPath($settings, 'autoplay', 'yes') === 'yes') { + $extras .= ' autoplay'; + } + + if (arrayPath($settings, 'loop', 'yes') === 'yes') { + $extras .= ' loop'; + } + + if (arrayPath($settings, 'muted', 'yes') === 'yes') { + $extras .= ' muted'; + } + + if (arrayPath($settings, 'controls', 'yes') === 'yes') { + $extras .= ' controls'; + } + + if (arrayPath($settings, 'playsinline', 'yes') === 'yes') { + $extras .= ' playsinline'; + } + + $preload = arrayPath($settings, 'preload', 'metadata'); + $extras .= " preload='{$preload}'"; + + $aspectClass = 'mux-ele-video-container'; + if ($asset) { + $aspect = generateAspectRatio($asset->width, $asset->height); + $aspectClass .= ' aspect-'.implode('-', $aspect); + } + + echo << + + {$metadataHTML} + +RENDER; + + } + + public static function filterContent($content) { + $vidregex = '/<\s*figure\s+class=\"\s*mux-ele-video-container(?:[^>]+)>\s*(]+>)\s*((<(?:source|track)[^>]+>)+)/ms'; + if (preg_match_all($vidregex, $content, $matches, PREG_SET_ORDER, 0)) { + foreach($matches as $match) { + $video = $match[1]; + $currentSource = $match[2]; + + if (preg_match_all('/data-attachment-id\s*=\s*(?:\'|")([^\'"]+)/ms', $video, $idMatches)) { + $attachmentId = $idMatches[1][0]; + /** @var VideoPlayerTool $playerTool */ + $playerTool = ToolsManager::instance()->tools['video-player']; + ['source' => $source] = $playerTool->videoPlayerProps($attachmentId); + $content = str_replace($currentSource, $source, $content); + } else if (preg_match_all('/data-mux-id\s*=\s*(?:\'|")([^\'"]+)/ms', $video, $idMatches)) { + $assetId = $idMatches[1][0]; + $asset = MuxAsset::asset($assetId); + if (!empty($asset)) { + $newUrl = $asset->videoUrl(); + $content = str_replace($currentSource, "", $content); + } + } + } + } + + return $content; + } +} \ No newline at end of file diff --git a/classes/Tools/Video/Player/Tool/VideoPlayerTool.php b/classes/Tools/Video/Player/Tool/VideoPlayerTool.php new file mode 100644 index 00000000..b9376603 --- /dev/null +++ b/classes/Tools/Video/Player/Tool/VideoPlayerTool.php @@ -0,0 +1,396 @@ +settings = VideoPlayerToolSettings::instance(); + + parent::__construct($toolName, $toolInfo, $toolManager); + } + + //region Tool Overrides + public function hasSettings() { + return true; + } + + public function setup() { + if ($this->enabled()) { + MuxDatabase::init(); + + $this->shortCode = new VideoPlayerShortcode(); + + add_filter('render_block', [$this, 'filterBlocks'], PHP_INT_MAX - 1, 2); + + static::enqueuePlayer(is_admin()); + + if (is_admin()) { + add_action('admin_enqueue_scripts', function(){ + wp_enqueue_script('mux-admin-js', ILAB_PUB_JS_URL.'/mux-admin.js', null, null, true); + wp_enqueue_style('mux-admin-css', ILAB_PUB_CSS_URL . '/mux-admin.css' ); + }); + + + $this->integrateWithAdmin(); + } + + $this->initBlocks(); + $this->initShortcodeOverride(); + } + } + //endregion + + //region UI + private function integrateWithAdmin() { + } + //endregion + + //region Video Properties + public function videoPlayerProps($attachmentId, $block = []) { + if (empty($attachmentId) || !postIdExists($attachmentId)) { + return null; + } + + $asset = null; + $renditionUrl = null; + $playlistUrl = null; + $originalUrl = wp_get_attachment_url($attachmentId); + $playlistMime = 'video/mp4'; + $asset = MuxAsset::assetForAttachment($attachmentId); + + if ($asset && $asset->isTransferred && !empty($asset->transferData)) { + if ($asset->transferData['source'] === 's3') { + /** @var StorageTool $storageTool */ + $storageTool = ToolsManager::instance()->tools['storage']; + $playlistUrl = $storageTool->getAttachmentURLFromMeta(['type' => 'application/vnd.apple.mpegurl', 's3' => ['key' => $asset->transferData['playlist'],]]); + $playlistMime = 'application/vnd.apple.mpegurl'; + + if(arrayPathExists($asset->transferData, 'renditions/' . $this->settings->playerMP4Quality)) { + $renditionKey = arrayPath($asset->transferData, 'renditions/' . $this->settings->playerMP4Quality); + $renditionUrl = $storageTool->getAttachmentURLFromMeta(['type' => 'video/mp4', 's3' => ['key' => $renditionKey,]]); + } + } else { + $uploadDir = wp_upload_dir(); + $playlistUrl = trailingslashit($uploadDir['baseurl']) . $asset->transferData['playlist']; + $playlistMime = 'application/vnd.apple.mpegurl'; + + if(arrayPathExists($asset->transferData, 'renditions/' . $this->settings->playerMP4Quality)) { + $renditionUrl = trailingslashit($uploadDir['baseurl']) . arrayPath($asset->transferData, 'renditions/' . $this->settings->playerMP4Quality); + } + } + } else { + if (!empty($asset)) { + $renditionUrl = $asset->renditionUrl($this->settings->playerMP4Quality); + $playlistUrl = $asset->videoUrl(); + $playlistMime = 'application/vnd.apple.mpegurl'; + } else { + $playlistUrl = $originalUrl; + } + } + + + $classes = "mux-player"; + $extras = ""; + $metadata = []; + $metadataKey = sanitize_title(gen_uuid(12)); + $meta = wp_get_attachment_metadata($attachmentId); + if (arrayPath($block, 'attrs/outputDimensions', false)) { + if (arrayPath($meta, 'width', 0) === 0) { + $thumbID = get_post_meta($attachmentId, '_thumbnail_id', true); + if (!empty($thumbID)) { + $thumbMeta = wp_get_attachment_metadata($thumbID); + if (!empty($thumbMeta)) { + $width = arrayPath($thumbMeta, 'width', 0); + $height = arrayPath($thumbMeta, 'height', 0); + if (($width !== 0) && ($height !== 0)) { + $extras .= "data-aspect-ratio='{$width}:{$height}'"; + } + } + } + } + } + + if (!empty($this->settings->playerCSSClasses)) { + $classes .= " {$this->settings->playerCSSClasses}"; + } + + $source = ""; + + if ($asset) { + foreach($asset->subtitles as $subtitle) { + if ($subtitle['local']) { + $subtitleKind = $subtitle['cc'] ? 'captions' : 'subtitles'; + $source .= ""; + } + } + } + + $metadataHTML = null; + if (!empty($metadata)) { + $metadataHTML = ""; + } + + return [ + 'classes' => $classes, + 'extras' => $extras, + 'playlistUrl' => $playlistUrl, + 'playlistMime' => $playlistMime, + 'renditionUrl' => $renditionUrl, + 'source' => $source, + 'metadata' => $metadata, + 'metadataHTML' => $metadataHTML, + 'metadataKey' => $metadataKey, + 'asset' => $asset, + ]; + } + //endregion + + //region Shortcode Override + private function initShortcodeOverride() { + if (!is_admin() && $this->settings->playerOverrideShortcode && ($this->settings->playerType !== 'none')) { + add_filter( 'wp_video_shortcode', function($output, $atts, $video, $post_id, $library) { + if (!isset($atts['mp4'])) { + return $output; + } + + $attachmentId = attachment_url_to_postid($atts['mp4']); + if (empty($attachmentId)) { + return $output; + } + + [ + 'classes' => $classes, + 'extras' => $extras, + 'source' => $source, + 'metadataHTML' => $metadataHTML, + 'metadataKey' => $metadataKey + ] = $this->videoPlayerProps($attachmentId); + + $output = str_replace('class="wp-video-shortcode"', "class='{$classes}' $extras ", $output); + + $sourceRegex = '/]+)\>/m'; + + $output = preg_replace($sourceRegex, $source, $output); + + if (!empty($metadataHTML)) { + $output .= "\n".$metadataHTML; + } + + return $output; + + }, PHP_INT_MAX, 5); + } + } + //endregion + + //region Player + public static function enqueuePlayer($admin = false) { + add_action((!empty($admin)) ? 'admin_enqueue_scripts' : 'wp_enqueue_scripts', function() { + wp_enqueue_script('mux_video_player_hlsjs', ILAB_PUB_JS_URL . '/mux-hls.js', null, null, true); + }); + } + //endregion + + //region Blocks + protected function initBlocks() { + add_action('init', function() { + register_block_type( ILAB_BLOCKS_DIR . '/mediacloud-video-block' ); +// register_block_type( ILAB_BLOCKS_DIR . '/mediacloud-video-block/build' ); + }); + + add_filter('block_categories', function($categories, $post) { + foreach($categories as $category) { + if ($category['slug'] === 'mediacloud') { + return $categories; + } + } + + $categories[] = [ + 'slug' => 'mediacloud', + 'title' => 'Media Cloud', + 'icon' => null + ]; + + return $categories; + }, 10, 2); + + + if (class_exists('Elementor\Plugin')) { + add_action('elementor/widgets/widgets_registered', function() { + Plugin::instance()->widgets_manager->register(new MediaCloudVideoWidget()); + }); + + add_action('elementor/elements/categories_registered', function($elementsManager) { + /** @var Elements_Manager $elementsManager */ + $elementsManager->add_category('media-cloud', [ + 'title' => 'Media Cloud', + 'icon' => 'fa fa-plug' + ]); + }, 10, 1); + + add_filter('the_content', function($content) { + return MediaCloudVideoWidget::filterContent($content); + }, PHP_INT_MAX, 1); + + add_action('wp_enqueue_scripts', function() { + wp_enqueue_style('mcloud-elementor', trailingslashit(ILAB_PUB_CSS_URL).'mcloud-elementor.css', [], MEDIA_CLOUD_VERSION); + }); + } + } + //endregion + + //region Content Filters + /** + * Filters the File block to include the goddamn attachment ID + * + * @param $block_content + * @param $block + * + * @return mixed + * @throws \Exception + */ + function filterBlocks($block_content, $block) { + if (isset($block['blockName'])) { + if ($block['blockName'] === 'media-cloud/mux-video-block') { + return $this->filterVideoBlock($block_content, $block); + } + } + + return $block_content; + } + + protected function filterVideoBlock($block_content, $block) { + $attachmentId = arrayPath($block, 'attrs/id', null); + if (empty($attachmentId) || !postIdExists($attachmentId)) { + return ''; + } + + $asset = null; + $renditionUrl = null; + $playlistUrl = null; + $originalUrl = wp_get_attachment_url($attachmentId); + $playlistMime = 'video/mp4'; + $muxId = arrayPath($block, 'attrs/muxId', null); + if (!empty($muxId)) { + $asset = MuxAsset::asset($muxId); + } + + if (!$asset) { + $asset = MuxAsset::assetForAttachment($attachmentId); + } + + if ($asset && !empty($asset->transferData)) { + if ($asset->transferData['source'] === 's3') { + /** @var StorageTool $storageTool */ + $storageTool = ToolsManager::instance()->tools['storage']; + $playlistUrl = $storageTool->getAttachmentURLFromMeta(['type' => 'application/vnd.apple.mpegurl', 's3' => ['key' => $asset->transferData['playlist'],]]); + $playlistMime = 'application/vnd.apple.mpegurl'; + + if(arrayPathExists($asset->transferData, 'renditions/' . $this->settings->playerMP4Quality)) { + $renditionKey = arrayPath($asset->transferData, 'renditions/' . $this->settings->playerMP4Quality); + $renditionUrl = $storageTool->getAttachmentURLFromMeta(['type' => 'video/mp4', 's3' => ['key' => $renditionKey,]]); + } + } else { + $uploadDir = wp_upload_dir(); + $playlistUrl = trailingslashit($uploadDir['baseurl']) . $asset->transferData['playlist']; + $playlistMime = 'application/vnd.apple.mpegurl'; + + if(arrayPathExists($asset->transferData, 'renditions/' . $this->settings->playerMP4Quality)) { + $renditionUrl = trailingslashit($uploadDir['baseurl']) . arrayPath($asset->transferData, 'renditions/' . $this->settings->playerMP4Quality); + } + } + } else { + if (!empty($asset)) { + $renditionUrl = $asset->renditionUrl($this->settings->playerMP4Quality); + $playlistUrl = $asset->videoUrl(); + $playlistMime = 'application/vnd.apple.mpegurl'; + } else { + $playlistUrl = $originalUrl; + } + } + + + $classes = "mux-player"; + $extras = ""; + $metadata = []; + $metadataKey = sanitize_title(gen_uuid(12)); + $meta = wp_get_attachment_metadata($attachmentId); + if (arrayPath($block, 'attrs/outputDimensions', false)) { + if (arrayPath($meta, 'width', 0) === 0) { + $thumbID = get_post_meta($attachmentId, '_thumbnail_id', true); + if (!empty($thumbID)) { + $thumbMeta = wp_get_attachment_metadata($thumbID); + if (!empty($thumbMeta)) { + $width = arrayPath($thumbMeta, 'width', 0); + $height = arrayPath($thumbMeta, 'height', 0); + if (($width !== 0) && ($height !== 0)) { + $extras .= "data-aspect-ratio='{$width}:{$height}'"; + } + } + } + } + } + + if (!empty($this->settings->playerCSSClasses)) { + $classes .= " {$this->settings->playerCSSClasses}"; + } + +// $styles = !empty($styles) ? "style='${styles}'" : ""; + $block_content = str_replace('