| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCommunity\Modules\Integrations\FluentPlayer\Http\Controllers; |
| 4 |
|
| 5 |
use FluentCommunity\App\Http\Controllers\Controller; |
| 6 |
use FluentCommunity\App\Models\Feed; |
| 7 |
use FluentCommunity\App\Models\Media; |
| 8 |
use FluentCommunity\App\Services\Helper; |
| 9 |
use FluentCommunity\App\Services\Libs\FileSystem; |
| 10 |
use FluentCommunity\Framework\Http\Request\Request; |
| 11 |
use FluentCommunity\Framework\Support\Arr; |
| 12 |
use FluentCommunity\Modules\Integrations\FluentPlayer\Bootstrap; |
| 13 |
|
| 14 |
class MediaController extends Controller |
| 15 |
{ |
| 16 |
public function uploadVideo(Request $request) |
| 17 |
{ |
| 18 |
$getFluentPlayerStatus = Bootstrap::getPluginStatus(); |
| 19 |
if ($getFluentPlayerStatus !== 'active') { |
| 20 |
return $this->sendError([ |
| 21 |
'message' => __('Uploading videos is not allowed. Activate FluentPlayer to upload videos.', 'fluent-community') |
| 22 |
]); |
| 23 |
} |
| 24 |
|
| 25 |
$fluentPlayerSettings = Bootstrap::getSettings(); |
| 26 |
if (!Arr::isTrue($fluentPlayerSettings, 'video_upload')) { |
| 27 |
return $this->sendError([ |
| 28 |
'message' => __('Uploading videos is not allowed', 'fluent-community') |
| 29 |
]); |
| 30 |
} |
| 31 |
$uploadRole = Arr::get($fluentPlayerSettings, 'video_upload_role'); |
| 32 |
$uploadRole = $uploadRole ?: 'admin'; |
| 33 |
$isAllowed = false; |
| 34 |
if ('admin' === $uploadRole && Helper::isSiteAdmin()) { |
| 35 |
$isAllowed = true; |
| 36 |
} else if ('admin_moderator' === $uploadRole && (Helper::isModerator() || Helper::isSiteAdmin())) { |
| 37 |
$isAllowed = true; |
| 38 |
} else if ('everyone' === $uploadRole) { |
| 39 |
$isAllowed = true; |
| 40 |
} |
| 41 |
if (!$isAllowed) { |
| 42 |
return $this->sendError([ |
| 43 |
'message' => __('You do not have permission to upload videos', 'fluent-community') |
| 44 |
]); |
| 45 |
} |
| 46 |
|
| 47 |
$hasAudioUpload = Arr::get($fluentPlayerSettings, 'enable_audio') === 'yes'; |
| 48 |
$mediaKind = $request->getSafe('media_kind', 'sanitize_text_field', ''); |
| 49 |
if (!in_array($mediaKind, ['video', 'audio'], true)) { |
| 50 |
$mediaKind = null; |
| 51 |
} |
| 52 |
|
| 53 |
if ($mediaKind === 'audio' && !$hasAudioUpload) { |
| 54 |
return $this->sendError([ |
| 55 |
'message' => __('Uploading audio files is not allowed', 'fluent-community') |
| 56 |
]); |
| 57 |
} |
| 58 |
|
| 59 |
$allowedMediaTypes = Bootstrap::getAllowedMediaTypes($fluentPlayerSettings, $mediaKind); |
| 60 |
$maxFileUnit = apply_filters('fluent_community/video_upload_max_file_unit', 'MB'); |
| 61 |
$maxFileSize = apply_filters('fluent_community/video_upload_max_file_size', 300); |
| 62 |
$allowedFileSize = $maxFileSize; |
| 63 |
if (strtoupper($maxFileUnit) == 'MB') { |
| 64 |
$allowedFileSize = $maxFileSize * 1024; |
| 65 |
} else if (strtoupper($maxFileUnit) == 'GB') { |
| 66 |
$allowedFileSize = $maxFileSize * 1024 * 1024; |
| 67 |
} |
| 68 |
|
| 69 |
$allowedTypesString = implode(',', $allowedMediaTypes); |
| 70 |
|
| 71 |
$files = $this->validate($request->files(), [ |
| 72 |
'file' => 'mimetypes:' . $allowedTypesString . '|max:' . $allowedFileSize, |
| 73 |
], [ |
| 74 |
'file.mimetypes' => $this->getMimeTypeErrorMessage($mediaKind, $hasAudioUpload), |
| 75 |
/* translators: %1$s is the maximum file size value, %2$s is the size unit (KB, MB, etc.) */ |
| 76 |
'file.max' => sprintf(__('The file size must be less than %1$s%2$s.', 'fluent-community'), $maxFileSize, $maxFileUnit) |
| 77 |
]); |
| 78 |
// File size check |
| 79 |
if ($error = Helper::checkUploadSizeError()) { |
| 80 |
return $this->sendError($error, 413); |
| 81 |
} |
| 82 |
|
| 83 |
$uploadedFiles = FileSystem::put($files); |
| 84 |
$file = $uploadedFiles[0]; |
| 85 |
|
| 86 |
$upload_dir = wp_upload_dir(); |
| 87 |
$file['path'] = $upload_dir['basedir'] . '/fluent-community/' . $file['file']; |
| 88 |
|
| 89 |
$mediaData = [ |
| 90 |
'media_type' => 'fluent_player', |
| 91 |
'driver' => 'local', |
| 92 |
'media_path' => $file['path'], |
| 93 |
'media_url' => $file['url'], |
| 94 |
'settings' => [ |
| 95 |
'src' => $file['url'], |
| 96 |
'title' => $file['original_name'], |
| 97 |
'viewType' => $this->resolveViewType($file, $mediaKind) |
| 98 |
] |
| 99 |
]; |
| 100 |
|
| 101 |
$mediaData = apply_filters('fluent_community/media_upload_data', $mediaData, $file); |
| 102 |
|
| 103 |
if (is_wp_error($mediaData)) { |
| 104 |
return $this->sendError([ |
| 105 |
'message' => $mediaData->get_error_message(), |
| 106 |
'errors' => $mediaData->get_error_data() |
| 107 |
]); |
| 108 |
} |
| 109 |
|
| 110 |
if (!$mediaData) { |
| 111 |
return $this->sendError([ |
| 112 |
'message' => __('Error while uploading the video', 'fluent-community') |
| 113 |
]); |
| 114 |
} |
| 115 |
$mediaData['settings']['src'] = Arr::get($mediaData, 'media_url', $file['url']); |
| 116 |
|
| 117 |
// Disable crossorigin for cloud storage drivers that may lack CORS headers |
| 118 |
$driver = Arr::get($mediaData, 'driver', 'local'); |
| 119 |
if ($driver === 's3') { |
| 120 |
$mediaData['settings']['crossorigin'] = false; |
| 121 |
} |
| 122 |
|
| 123 |
$media = Media::create($mediaData); |
| 124 |
return [ |
| 125 |
'media' => [ |
| 126 |
'media_id' => $media->id, |
| 127 |
'url' => $media->public_url, |
| 128 |
'media_key' => $media->media_key, |
| 129 |
'type' => $media->media_type, |
| 130 |
'settings' => $media->settings, |
| 131 |
'html' => '' |
| 132 |
] |
| 133 |
]; |
| 134 |
} |
| 135 |
|
| 136 |
/** |
| 137 |
* Whether the current user may edit an audio media's metadata. |
| 138 |
* Owner (upload sets user_id) or a moderator/site admin. |
| 139 |
*/ |
| 140 |
public function canEditAudioMedia($media) |
| 141 |
{ |
| 142 |
if (!$media) { |
| 143 |
return false; |
| 144 |
} |
| 145 |
// Read user_id via the model accessor. A (array) cast of a WPFluent model exposes |
| 146 |
// the protected $attributes under a mangled key, not a flat user_id. |
| 147 |
if ((int) $media->user_id === (int) get_current_user_id()) { |
| 148 |
return true; |
| 149 |
} |
| 150 |
return Helper::isModerator() || Helper::isSiteAdmin(); |
| 151 |
} |
| 152 |
|
| 153 |
/** |
| 154 |
* Update an audio media's display title and poster thumbnail from the composer. |
| 155 |
*/ |
| 156 |
public function updateAudioMeta(Request $request) |
| 157 |
{ |
| 158 |
$media = Media::find(intval($request->get('media_id'))); |
| 159 |
if (!$media || !$this->canEditAudioMedia($media)) { |
| 160 |
return $this->sendError([ |
| 161 |
'message' => __('You are not allowed to edit this audio', 'fluent-community'), |
| 162 |
], 403); |
| 163 |
} |
| 164 |
|
| 165 |
// Only fluent-player audio media may be edited through this endpoint. |
| 166 |
if ($media->media_type !== 'fluent_player' || !$this->isAudioMedia((array) $media->settings)) { |
| 167 |
return $this->sendError([ |
| 168 |
'message' => __('This media is not an editable audio', 'fluent-community'), |
| 169 |
], 422); |
| 170 |
} |
| 171 |
|
| 172 |
$settings = (array) $media->settings; |
| 173 |
$title = $request->getSafe('title', 'sanitize_text_field', ''); |
| 174 |
$posterSrc = $request->getSafe('posterSrc', 'sanitize_url', ''); |
| 175 |
|
| 176 |
if ($title !== '') { |
| 177 |
$settings['title'] = $title; |
| 178 |
} |
| 179 |
if ($posterSrc !== '') { |
| 180 |
$settings['posterSrc'] = $posterSrc; |
| 181 |
// poster_is_custom gates whether the render path shows the custom thumbnail |
| 182 |
// (see show_poster in generateFluentPlayerHtml). |
| 183 |
$settings['poster_is_custom'] = true; |
| 184 |
} else { |
| 185 |
$settings['posterSrc'] = ''; |
| 186 |
$settings['poster_is_custom'] = false; |
| 187 |
} |
| 188 |
|
| 189 |
$media->settings = $settings; |
| 190 |
$media->save(); |
| 191 |
|
| 192 |
return [ |
| 193 |
'media' => [ |
| 194 |
'media_id' => $media->id, |
| 195 |
'settings' => $media->settings, |
| 196 |
], |
| 197 |
]; |
| 198 |
} |
| 199 |
|
| 200 |
private function resolveViewType($file, $mediaKind) |
| 201 |
{ |
| 202 |
$audioExtensions = ['mp3', 'wav', 'm4a', 'aac', 'ogg', 'oga', 'flac']; |
| 203 |
$name = Arr::get($file, 'original_name', Arr::get($file, 'url', '')); |
| 204 |
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION)); |
| 205 |
|
| 206 |
if (in_array($extension, $audioExtensions, true)) { |
| 207 |
return 'audio'; |
| 208 |
} |
| 209 |
|
| 210 |
if ($extension) { |
| 211 |
return 'video'; |
| 212 |
} |
| 213 |
|
| 214 |
return $mediaKind === 'audio' ? 'audio' : 'video'; |
| 215 |
} |
| 216 |
|
| 217 |
private function getMimeTypeErrorMessage($mediaKind, $hasAudioUpload) |
| 218 |
{ |
| 219 |
if ($mediaKind === 'audio') { |
| 220 |
return __('The file must be a valid audio (MP3, WAV, M4A, AAC, OGG, FLAC) type.', 'fluent-community'); |
| 221 |
} |
| 222 |
|
| 223 |
if ($mediaKind === 'video' || !$hasAudioUpload) { |
| 224 |
return __('The file must be a valid video (MP4, WebM, MOV) type.', 'fluent-community'); |
| 225 |
} |
| 226 |
|
| 227 |
return __('The file must be a valid video (MP4, WebM, MOV) or audio (MP3, WAV, M4A, AAC, OGG, FLAC) type.', 'fluent-community'); |
| 228 |
} |
| 229 |
|
| 230 |
public function getFluentPlayerContent(Request $request) |
| 231 |
{ |
| 232 |
if (!defined('FLUENT_PLAYER_VERSION')) { |
| 233 |
return [ |
| 234 |
'html' => '', |
| 235 |
]; |
| 236 |
} |
| 237 |
$mediaId = intval($request->get('media_id')); |
| 238 |
$instanceKey = sanitize_key($request->get('player_instance_key')); |
| 239 |
$shareUrl = esc_url_raw((string) $request->get('share_url', '')); |
| 240 |
$media = Media::find($mediaId); |
| 241 |
if (!$media) { |
| 242 |
// Non-DB media (external embed / share URL) from a synthetic hash id: build from |
| 243 |
// allowlisted scalars only. Never trust request `settings`/`layers` — accepting |
| 244 |
// them lets an anon caller inject shortcode layers into FluentPlayer's renderer. |
| 245 |
$media = (object) [ |
| 246 |
'url' => esc_url_raw((string) $request->get('url', '')), |
| 247 |
'title' => sanitize_text_field($request->get('title')), |
| 248 |
'image' => esc_url_raw((string) $request->get('image', '')), |
| 249 |
'share_url' => esc_url_raw((string) $request->get('share_url', '')), |
| 250 |
'provider' => sanitize_text_field($request->get('provider')), |
| 251 |
'type' => sanitize_text_field($request->get('type')), |
| 252 |
]; |
| 253 |
} else { |
| 254 |
$currentUserId = get_current_user_id(); |
| 255 |
$canView = ($currentUserId && (int) $media->user_id === $currentUserId) |
| 256 |
|| ($media->feed_id && Feed::byUserAccess($currentUserId) |
| 257 |
->byContentModerationAccessStatus($this->getUser()) |
| 258 |
->find($media->feed_id)); |
| 259 |
if (!$canView) { |
| 260 |
return ['html' => '']; |
| 261 |
} |
| 262 |
if ($shareUrl) { |
| 263 |
$media->share_url = $shareUrl; |
| 264 |
} |
| 265 |
} |
| 266 |
return $this->generateFluentPlayerHtml($mediaId, $media, $instanceKey); |
| 267 |
} |
| 268 |
|
| 269 |
private function generateFluentPlayerHtml($mediaId, $media, $instanceKey = '') |
| 270 |
{ |
| 271 |
$mediaVarName = 'fluentPlayerMedia_' . $mediaId . '_' . wp_rand(1000, 9999); |
| 272 |
$playerKey = 'fcom_' . $mediaId; |
| 273 |
$instanceId = $playerKey . ($instanceKey ? '_' . $instanceKey : ''); |
| 274 |
$mediaSettings = []; |
| 275 |
$formattedMedia = [ |
| 276 |
'ID' => $mediaId |
| 277 |
]; |
| 278 |
if (!empty($media->settings)) { |
| 279 |
$mediaSettings = $media->settings; |
| 280 |
} |
| 281 |
if (empty($mediaSettings['src']) && !empty($media->url)) { |
| 282 |
$mediaSettings['src'] = $media->url; |
| 283 |
} |
| 284 |
if (empty($mediaSettings['title']) && !empty($media->title)) { |
| 285 |
$mediaSettings['title'] = $media->title; |
| 286 |
} |
| 287 |
if (empty($mediaSettings['posterSrc']) && !empty($media->image)) { |
| 288 |
$mediaSettings['posterSrc'] = $media->image; |
| 289 |
} |
| 290 |
if (empty($mediaSettings['share_url']) && !empty($media->share_url)) { |
| 291 |
$mediaSettings['share_url'] = esc_url_raw($media->share_url); |
| 292 |
} |
| 293 |
$src = Arr::get($mediaSettings, 'src', ''); |
| 294 |
// Normalize YouTube /live/ URLs to /watch?v= so Vidstack can resolve them |
| 295 |
if ($src && strpos($src, 'youtube.com/live/') !== false) { |
| 296 |
$src = preg_replace('/youtube\.com\/live\/([^?&#]+)\??/', 'youtube.com/watch?v=$1&', $src); |
| 297 |
$src = rtrim($src, '&'); |
| 298 |
$mediaSettings['src'] = $src; |
| 299 |
} |
| 300 |
if ($src && preg_match('#youtube\.com/shorts/#i', $src)) { |
| 301 |
$mediaSettings['is_short'] = true; |
| 302 |
} |
| 303 |
|
| 304 |
$mediaSettings = wp_parse_args($mediaSettings, $this->getFluentplayerDefaultsSettings()); |
| 305 |
|
| 306 |
if ($this->isAudioMedia($mediaSettings)) { |
| 307 |
// Only show a thumbnail block when the author uploaded one; otherwise render a |
| 308 |
// clean player with no poster (per design). poster_is_custom is set by the |
| 309 |
// audio edit modal / updateAudioMeta. |
| 310 |
$hasCustomPoster = Arr::get($mediaSettings, 'posterSrc', '') !== '' |
| 311 |
&& filter_var(Arr::get($mediaSettings, 'poster_is_custom'), FILTER_VALIDATE_BOOLEAN); |
| 312 |
$mediaSettings['show_poster'] = $hasCustomPoster; |
| 313 |
// Show the inline "1x" speed control instead of the settings gear menu. |
| 314 |
if (!isset($mediaSettings['controls']) || !is_array($mediaSettings['controls'])) { |
| 315 |
$mediaSettings['controls'] = []; |
| 316 |
} |
| 317 |
$mediaSettings['controls']['settings'] = false; |
| 318 |
} |
| 319 |
|
| 320 |
$formattedMedia['settings'] = $mediaSettings; |
| 321 |
$formattedMedia['deep_link_ref'] = $playerKey; |
| 322 |
|
| 323 |
// Generate the HTML using fluent-player's view system |
| 324 |
ob_start(); |
| 325 |
if (class_exists('\FluentPlayer\App\App')) { |
| 326 |
try { |
| 327 |
$fluentPlayerApp = \FluentPlayer\App\App::getInstance(); |
| 328 |
if ($fluentPlayerApp) { |
| 329 |
$fluentPlayerApp->view->render('player', [ |
| 330 |
'media_id' => $mediaId, |
| 331 |
'instance_id' => $instanceId, |
| 332 |
'media_var_name' => $mediaVarName, |
| 333 |
'player_ref' => $playerKey, |
| 334 |
'settings' => $mediaSettings |
| 335 |
]); |
| 336 |
} |
| 337 |
} catch (\Exception $e) { |
| 338 |
} |
| 339 |
} |
| 340 |
$html = ob_get_clean(); |
| 341 |
return [ |
| 342 |
'html' => $html ? $html . $this->generateFluentPlayerCustomStyle($instanceId, $mediaSettings) : '', |
| 343 |
'media' => $formattedMedia |
| 344 |
]; |
| 345 |
} |
| 346 |
|
| 347 |
private function isAudioMedia($settings) |
| 348 |
{ |
| 349 |
if (class_exists('\FluentPlayer\App\Helpers\Helper') |
| 350 |
&& method_exists('\FluentPlayer\App\Helpers\Helper', 'isAudioSource')) { |
| 351 |
return \FluentPlayer\App\Helpers\Helper::isAudioSource($settings); |
| 352 |
} |
| 353 |
return Arr::get($settings, 'viewType') === 'audio'; |
| 354 |
} |
| 355 |
|
| 356 |
private function generateFluentPlayerCustomStyle($instanceId, $settings) |
| 357 |
{ |
| 358 |
$customCss = ''; |
| 359 |
$isAudio = $this->isAudioMedia($settings); |
| 360 |
$playerWidth = intval(Arr::get($settings, 'playerWidth')); |
| 361 |
if ($playerWidth > 0) { |
| 362 |
$customCss .= " |
| 363 |
#fluent_player_" . esc_attr($instanceId) . " .fluent-player-container { |
| 364 |
width: " . $playerWidth . 'px' . "; |
| 365 |
} |
| 366 |
"; |
| 367 |
} |
| 368 |
$colorVars = array_filter([ |
| 369 |
'--media-brand' => Bootstrap::sanitizeColor(Arr::get($settings, 'brandColor', '')), |
| 370 |
'--fp-control-bar-bg' => Bootstrap::sanitizeColor(Arr::get($settings, 'controlBarColor', '')), |
| 371 |
'--media-play-color' => Bootstrap::sanitizeColor(Arr::get($settings, 'playButtonColor', '')), |
| 372 |
'--media-play-bg' => Bootstrap::sanitizeColor(Arr::get($settings, 'playButtonBgColor', '')) |
| 373 |
]); |
| 374 |
if ($colorVars) { |
| 375 |
$customCss .= " |
| 376 |
#fluent_player_" . esc_attr($instanceId) . " {"; |
| 377 |
foreach ($colorVars as $varName => $varValue) { |
| 378 |
$customCss .= " |
| 379 |
" . $varName . ": " . esc_attr($varValue) . ";"; |
| 380 |
} |
| 381 |
$customCss .= " |
| 382 |
} |
| 383 |
"; |
| 384 |
} |
| 385 |
if (!empty(Arr::get($settings, 'posterSrc', ''))) { |
| 386 |
$customCss .= " |
| 387 |
#fluent_player_" . esc_attr($instanceId) . " .fluent-player-container { |
| 388 |
background-image: url('" . esc_url(Arr::get($settings, 'posterSrc', '')) . "'); |
| 389 |
background-size: cover; |
| 390 |
background-position: center; |
| 391 |
min-height: 100%; |
| 392 |
} |
| 393 |
"; |
| 394 |
} |
| 395 |
|
| 396 |
if ($isAudio) { |
| 397 |
// Audio follows the FluentCommunity color schema (Utility::getColorSchemas()). |
| 398 |
// The player renders inside the portal DOM and inherits the --fcom-* CSS vars that |
| 399 |
// generateCss() emits on body (and html.dark body), so theme/skin/dark changes recolor |
| 400 |
// audio live with no re-save. The stock "modern" audio skin is built for a dark |
| 401 |
// background (white title, translucent-white controls, brand-colored bar); we override |
| 402 |
// those to a flat, theme-surface card. Play circle + progress fill use the auto- |
| 403 |
// inverting primary_text/primary_bg pair so the same rules match a light card |
| 404 |
// (dark on light) and a dark card (light on dark) without per-theme branching. |
| 405 |
$playerId = '#fluent_player_' . esc_attr($instanceId); |
| 406 |
$customCss .= " |
| 407 |
{$playerId} { |
| 408 |
--media-brand: var(--fcom-primary-text, #19283a); |
| 409 |
--fp-control-bar-bg: transparent; |
| 410 |
} |
| 411 |
{$playerId} .fp-audio-layout { |
| 412 |
background-color: var(--fcom-secondary-content-bg, var(--fcom-active-bg, #f0f2f5)); |
| 413 |
border-radius: 12px; |
| 414 |
} |
| 415 |
{$playerId} .fp-audio-main-controls { |
| 416 |
background: transparent; |
| 417 |
} |
| 418 |
{$playerId} .fp-audio-title { |
| 419 |
color: var(--fcom-primary-text, #19283a); |
| 420 |
font-size: 14px; |
| 421 |
font-weight: 600; |
| 422 |
} |
| 423 |
{$playerId} .fp-audio-subtitle { |
| 424 |
color: var(--fcom-primary-text, #19283a); |
| 425 |
} |
| 426 |
{$playerId} media-play-button { |
| 427 |
background: var(--fcom-primary-text, #19283a); |
| 428 |
} |
| 429 |
{$playerId} media-play-button media-icon { |
| 430 |
color: var(--fcom-primary-bg, #ffffff); |
| 431 |
} |
| 432 |
{$playerId} media-seek-button { |
| 433 |
background: transparent; |
| 434 |
} |
| 435 |
{$playerId} media-time-slider .fp-media-slider-track { |
| 436 |
background: var(--fcom-secondary-border, #9CA3AF); |
| 437 |
background: color-mix(in srgb, var(--fcom-primary-text, #19283a) 20%, transparent); |
| 438 |
} |
| 439 |
{$playerId} media-time-slider .fp-media-slider-track-fill { |
| 440 |
background: var(--fcom-primary-text, #19283a); |
| 441 |
} |
| 442 |
{$playerId} .fp-seek-backward-10, |
| 443 |
{$playerId} .fp-seek-forward-10, |
| 444 |
{$playerId} .fp-media-mute-icon, |
| 445 |
{$playerId} .fp-media-volume-low-icon, |
| 446 |
{$playerId} .fp-media-volume-high-icon, |
| 447 |
{$playerId} media-seek-button media-icon, |
| 448 |
{$playerId} media-icon[type=\"odometer\"], |
| 449 |
{$playerId} media-icon[type=\"settings\"] { |
| 450 |
color: var(--fcom-secondary-text, #525866); |
| 451 |
} |
| 452 |
{$playerId} .fp-media-time-group, |
| 453 |
{$playerId} .fp-media-time-group media-time, |
| 454 |
{$playerId} .fp-media-time-divider { |
| 455 |
color: var(--fcom-primary-text, #19283a); |
| 456 |
} |
| 457 |
"; |
| 458 |
|
| 459 |
// Exact layout: large play circle on the left (spanning title + controls), |
| 460 |
// a single control row (seek / seek / volume / speed / time) and a full-width |
| 461 |
// progress bar at the bottom. The volume slider is hidden until hover. |
| 462 |
$customCss .= " |
| 463 |
{$playerId} .fp-audio-layout { |
| 464 |
position: relative; |
| 465 |
padding: 22px 24px 14px; |
| 466 |
} |
| 467 |
{$playerId} .fp-audio-layout .fp-audio-content { |
| 468 |
padding: 0 0 0 82px; |
| 469 |
gap: 6px; |
| 470 |
width: 100%; |
| 471 |
} |
| 472 |
{$playerId} .fp-audio-header { |
| 473 |
padding-left: 8px; |
| 474 |
} |
| 475 |
{$playerId} media-play-button { |
| 476 |
position: absolute; |
| 477 |
left: 22px; |
| 478 |
top: 50%; |
| 479 |
transform: translateY(-50%); |
| 480 |
width: 64px; |
| 481 |
height: 64px; |
| 482 |
min-width: 64px; |
| 483 |
} |
| 484 |
{$playerId} .fp-audio-main-controls { |
| 485 |
display: grid; |
| 486 |
grid-template-columns: max-content max-content max-content max-content 1fr max-content; |
| 487 |
grid-template-areas: \"sb sf vol spd play time\" \"pr pr pr pr pr pr\"; |
| 488 |
align-items: center; |
| 489 |
column-gap: 0; |
| 490 |
row-gap: 6px; |
| 491 |
padding: 0; |
| 492 |
width: 100%; |
| 493 |
} |
| 494 |
{$playerId} .fp-audio-main-controls media-tooltip:nth-of-type(1) { grid-area: sb; margin: 0; } |
| 495 |
{$playerId} .fp-audio-main-controls media-tooltip:nth-of-type(2) { grid-area: play; } |
| 496 |
{$playerId} .fp-audio-main-controls media-tooltip:nth-of-type(3) { grid-area: sf; margin: 0; } |
| 497 |
{$playerId} media-seek-button, |
| 498 |
{$playerId} .fp-audio-volume-group media-mute-button { |
| 499 |
width: 32px; |
| 500 |
height: 32px; |
| 501 |
min-width: 32px; |
| 502 |
} |
| 503 |
{$playerId} .fp-audio-secondary-controls media-menu-button { min-width: 32px; padding: 0 6px; } |
| 504 |
{$playerId} .fp-audio-volume-group { grid-area: vol; margin: 0; overflow: visible; } |
| 505 |
{$playerId} .fp-audio-secondary-controls { grid-area: spd; margin: 0; } |
| 506 |
{$playerId} .fp-media-time-group { grid-area: time; margin: 0; justify-self: end; } |
| 507 |
{$playerId} media-player[data-view-type=\"audio\"] media-time-slider, |
| 508 |
{$playerId} media-time-slider { grid-area: pr; margin: 8px; } |
| 509 |
{$playerId} media-seek-button:hover, |
| 510 |
{$playerId} .fp-audio-volume-group media-mute-button:hover, |
| 511 |
{$playerId} .fp-audio-secondary-controls media-menu-button:hover { |
| 512 |
background: transparent; |
| 513 |
} |
| 514 |
{$playerId} .fp-audio-volume-group media-volume-slider { |
| 515 |
width: 0; |
| 516 |
max-width: 0; |
| 517 |
min-width: 0; |
| 518 |
opacity: 0; |
| 519 |
overflow: hidden; |
| 520 |
margin: 0; |
| 521 |
transition: width .2s ease, opacity .2s ease, margin .2s ease; |
| 522 |
} |
| 523 |
{$playerId} .fp-audio-volume-group:hover media-volume-slider, |
| 524 |
{$playerId} .fp-audio-volume-group:focus-within media-volume-slider { |
| 525 |
width: 80px; |
| 526 |
max-width: 80px; |
| 527 |
opacity: 1; |
| 528 |
margin-left: 6px; |
| 529 |
} |
| 530 |
{$playerId} .fp-speed-menu .fcom_speed_label { |
| 531 |
font-weight: 600; |
| 532 |
font-size: 15px; |
| 533 |
line-height: 1; |
| 534 |
color: var(--fcom-primary-text, #19283a); |
| 535 |
} |
| 536 |
"; |
| 537 |
|
| 538 |
// When a custom thumbnail is set, lay the card out as poster -> play -> content |
| 539 |
// (matching the reference). The poster sits on the left, the play circle after it. |
| 540 |
$customCss .= " |
| 541 |
{$playerId} .fp-audio-layout:not(.fp-audio-no-poster) { |
| 542 |
gap: 16px; |
| 543 |
padding-left: 16px; |
| 544 |
} |
| 545 |
{$playerId} .fp-audio-layout:not(.fp-audio-no-poster) .fp-audio-poster { |
| 546 |
width: 96px; |
| 547 |
height: 96px; |
| 548 |
min-width: 96px; |
| 549 |
border-radius: 12px; |
| 550 |
} |
| 551 |
{$playerId} .fp-audio-layout:not(.fp-audio-no-poster) media-play-button { |
| 552 |
left: 128px; |
| 553 |
} |
| 554 |
{$playerId} .fp-audio-layout:not(.fp-audio-no-poster) .fp-audio-content { |
| 555 |
padding-left: 82px; |
| 556 |
} |
| 557 |
"; |
| 558 |
} |
| 559 |
|
| 560 |
$aspectRatio = Arr::get($settings, 'aspectRatio'); |
| 561 |
if ($aspectRatio && $aspectRatio != 'original' && preg_match('/^\d+:\d+$/', $aspectRatio)) { |
| 562 |
$cssAspectRatio = preg_replace('/^(\d+):(\d+)$/', '$1/$2', $aspectRatio); |
| 563 |
$customCss .= " |
| 564 |
#fluent_player_" . esc_attr($instanceId) . " { |
| 565 |
aspect-ratio: " . esc_attr($cssAspectRatio) . "; |
| 566 |
} |
| 567 |
#fluent_player_" . esc_attr($instanceId) . " media-player[data-view-type='video'] { |
| 568 |
aspect-ratio: " . esc_attr($cssAspectRatio) . "; |
| 569 |
} |
| 570 |
"; |
| 571 |
} else if (!$isAudio) { |
| 572 |
$customCss .= " |
| 573 |
#fluent_player_" . esc_attr($instanceId) . " { |
| 574 |
max-width: 100%; |
| 575 |
min-height: 300px; |
| 576 |
} |
| 577 |
|
| 578 |
@media (max-width: 768px) { |
| 579 |
#fluent_player_" . esc_attr($instanceId) . " { |
| 580 |
min-height: 200px; |
| 581 |
} |
| 582 |
} |
| 583 |
"; |
| 584 |
} |
| 585 |
if (!empty($customCss)) { |
| 586 |
return '<style>' . $customCss . '</style>'; |
| 587 |
} |
| 588 |
return ''; |
| 589 |
} |
| 590 |
|
| 591 |
private function getFluentplayerDefaultsSettings() |
| 592 |
{ |
| 593 |
$settings = [ |
| 594 |
'src' => '', |
| 595 |
'title' => '', |
| 596 |
'posterSrc' => '', |
| 597 |
'viewType' => 'video', |
| 598 |
'brandColor' => '#4a90e2', |
| 599 |
'aspectRatio' => 'original', |
| 600 |
'playerWidth' => '', |
| 601 |
'playsInline' => true |
| 602 |
]; |
| 603 |
$mediaSettings = Bootstrap::getSettings(); |
| 604 |
if (Arr::isTrue($mediaSettings, 'behaviors.muted_autoplay')) { |
| 605 |
$mediaSettings['autoplay'] = true; |
| 606 |
$mediaSettings['muted'] = true; |
| 607 |
} |
| 608 |
$settings['loadStrategy'] = 'idle'; // for SPA context this needs to be set to 'idle' |
| 609 |
$settings = array_merge($settings, $mediaSettings); |
| 610 |
|
| 611 |
// Apply iOS Safari compatibility (playsinline, preload; muted only when autoplay is on) |
| 612 |
if (class_exists('\FluentPlayer\App\Services\MediaService')) { |
| 613 |
[$isIosSafari, $iosSafariSettings] = \FluentPlayer\App\Services\MediaService::getIOSSafariSettings($settings); |
| 614 |
if ($isIosSafari) { |
| 615 |
$settings = array_merge($settings, $iosSafariSettings); |
| 616 |
} |
| 617 |
} |
| 618 |
|
| 619 |
return apply_filters('fluent_community/fluentplayer_defaults_settings', $settings); |
| 620 |
} |
| 621 |
} |
| 622 |
|