PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.7
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.7
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / Modules / Integrations / FluentPlayer / Http / Controllers / MediaController.php

MediaController.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.7.7, at Modules/Integrations/FluentPlayer/Http/Controllers/MediaController.php

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