PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.5
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.5
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.5, at Modules/Integrations/FluentPlayer/Http/Controllers/MediaController.php

607 lines 25.0 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 $brandingColor = Bootstrap::sanitizeColor(Arr::get($settings, 'brandColor', ''));
348 if ($brandingColor) {
349 $customCss .= "
350 #fluent_player_" . esc_attr($instanceId) . " {
351 --media-brand: " . esc_attr($brandingColor) . ";
352 }
353 ";
354 }
355 $controlBarColor = Bootstrap::sanitizeColor(Arr::get($settings, 'controlBarColor', ''));
356 if ($brandingColor || $controlBarColor) {
357 $customCss .= "
358 #fluent_player_" . esc_attr($instanceId) . " {";
359 if ($brandingColor) {
360 $customCss .= "
361 --media-brand: " . esc_attr($brandingColor) . ";";
362 }
363 if ($controlBarColor) {
364 $customCss .= "
365 --fp-control-bar-bg: " . esc_attr($controlBarColor) . ";";
366 }
367 $customCss .= "
368 }
369 ";
370 }
371 if (!empty(Arr::get($settings, 'posterSrc', ''))) {
372 $customCss .= "
373 #fluent_player_" . esc_attr($instanceId) . " .fluent-player-container {
374 background-image: url('" . esc_url(Arr::get($settings, 'posterSrc', '')) . "');
375 background-size: cover;
376 background-position: center;
377 min-height: 100%;
378 }
379 ";
380 }
381
382 if ($isAudio) {
383 // Audio follows the FluentCommunity color schema (Utility::getColorSchemas()).
384 // The player renders inside the portal DOM and inherits the --fcom-* CSS vars that
385 // generateCss() emits on body (and html.dark body), so theme/skin/dark changes recolor
386 // audio live with no re-save. The stock "modern" audio skin is built for a dark
387 // background (white title, translucent-white controls, brand-colored bar); we override
388 // those to a flat, theme-surface card. Play circle + progress fill use the auto-
389 // inverting primary_text/primary_bg pair so the same rules match a light card
390 // (dark on light) and a dark card (light on dark) without per-theme branching.
391 $playerId = '#fluent_player_' . esc_attr($instanceId);
392 $customCss .= "
393 {$playerId} {
394 --media-brand: var(--fcom-primary-text, #19283a);
395 --fp-control-bar-bg: transparent;
396 }
397 {$playerId} .fp-audio-layout {
398 background-color: var(--fcom-secondary-content-bg, var(--fcom-active-bg, #f0f2f5));
399 border-radius: 12px;
400 }
401 {$playerId} .fp-audio-main-controls {
402 background: transparent;
403 }
404 {$playerId} .fp-audio-title {
405 color: var(--fcom-primary-text, #19283a);
406 font-size: 14px;
407 font-weight: 600;
408 }
409 {$playerId} .fp-audio-subtitle {
410 color: var(--fcom-primary-text, #19283a);
411 }
412 {$playerId} media-play-button {
413 background: var(--fcom-primary-text, #19283a);
414 }
415 {$playerId} media-play-button media-icon {
416 color: var(--fcom-primary-bg, #ffffff);
417 }
418 {$playerId} media-seek-button {
419 background: transparent;
420 }
421 {$playerId} media-time-slider .fp-media-slider-track {
422 background: var(--fcom-secondary-border, #9CA3AF);
423 background: color-mix(in srgb, var(--fcom-primary-text, #19283a) 20%, transparent);
424 }
425 {$playerId} media-time-slider .fp-media-slider-track-fill {
426 background: var(--fcom-primary-text, #19283a);
427 }
428 {$playerId} .fp-seek-backward-10,
429 {$playerId} .fp-seek-forward-10,
430 {$playerId} .fp-media-mute-icon,
431 {$playerId} .fp-media-volume-low-icon,
432 {$playerId} .fp-media-volume-high-icon,
433 {$playerId} media-seek-button media-icon,
434 {$playerId} media-icon[type=\"odometer\"],
435 {$playerId} media-icon[type=\"settings\"] {
436 color: var(--fcom-secondary-text, #525866);
437 }
438 {$playerId} .fp-media-time-group,
439 {$playerId} .fp-media-time-group media-time,
440 {$playerId} .fp-media-time-divider {
441 color: var(--fcom-primary-text, #19283a);
442 }
443 ";
444
445 // Exact layout: large play circle on the left (spanning title + controls),
446 // a single control row (seek / seek / volume / speed / time) and a full-width
447 // progress bar at the bottom. The volume slider is hidden until hover.
448 $customCss .= "
449 {$playerId} .fp-audio-layout {
450 position: relative;
451 padding: 22px 24px 14px;
452 }
453 {$playerId} .fp-audio-layout .fp-audio-content {
454 padding: 0 0 0 82px;
455 gap: 6px;
456 width: 100%;
457 }
458 {$playerId} .fp-audio-header {
459 padding-left: 8px;
460 }
461 {$playerId} media-play-button {
462 position: absolute;
463 left: 22px;
464 top: 50%;
465 transform: translateY(-50%);
466 width: 64px;
467 height: 64px;
468 min-width: 64px;
469 }
470 {$playerId} .fp-audio-main-controls {
471 display: grid;
472 grid-template-columns: max-content max-content max-content max-content 1fr max-content;
473 grid-template-areas: \"sb sf vol spd play time\" \"pr pr pr pr pr pr\";
474 align-items: center;
475 column-gap: 0;
476 row-gap: 6px;
477 padding: 0;
478 width: 100%;
479 }
480 {$playerId} .fp-audio-main-controls media-tooltip:nth-of-type(1) { grid-area: sb; margin: 0; }
481 {$playerId} .fp-audio-main-controls media-tooltip:nth-of-type(2) { grid-area: play; }
482 {$playerId} .fp-audio-main-controls media-tooltip:nth-of-type(3) { grid-area: sf; margin: 0; }
483 {$playerId} media-seek-button,
484 {$playerId} .fp-audio-volume-group media-mute-button {
485 width: 32px;
486 height: 32px;
487 min-width: 32px;
488 }
489 {$playerId} .fp-audio-secondary-controls media-menu-button { min-width: 32px; padding: 0 6px; }
490 {$playerId} .fp-audio-volume-group { grid-area: vol; margin: 0; overflow: visible; }
491 {$playerId} .fp-audio-secondary-controls { grid-area: spd; margin: 0; }
492 {$playerId} .fp-media-time-group { grid-area: time; margin: 0; justify-self: end; }
493 {$playerId} media-player[data-view-type=\"audio\"] media-time-slider,
494 {$playerId} media-time-slider { grid-area: pr; margin: 8px; }
495 {$playerId} media-seek-button:hover,
496 {$playerId} .fp-audio-volume-group media-mute-button:hover,
497 {$playerId} .fp-audio-secondary-controls media-menu-button:hover {
498 background: transparent;
499 }
500 {$playerId} .fp-audio-volume-group media-volume-slider {
501 width: 0;
502 max-width: 0;
503 min-width: 0;
504 opacity: 0;
505 overflow: hidden;
506 margin: 0;
507 transition: width .2s ease, opacity .2s ease, margin .2s ease;
508 }
509 {$playerId} .fp-audio-volume-group:hover media-volume-slider,
510 {$playerId} .fp-audio-volume-group:focus-within media-volume-slider {
511 width: 80px;
512 max-width: 80px;
513 opacity: 1;
514 margin-left: 6px;
515 }
516 {$playerId} .fp-speed-menu .fcom_speed_label {
517 font-weight: 600;
518 font-size: 15px;
519 line-height: 1;
520 color: var(--fcom-primary-text, #19283a);
521 }
522 ";
523
524 // When a custom thumbnail is set, lay the card out as poster -> play -> content
525 // (matching the reference). The poster sits on the left, the play circle after it.
526 $customCss .= "
527 {$playerId} .fp-audio-layout:not(.fp-audio-no-poster) {
528 gap: 16px;
529 padding-left: 16px;
530 }
531 {$playerId} .fp-audio-layout:not(.fp-audio-no-poster) .fp-audio-poster {
532 width: 96px;
533 height: 96px;
534 min-width: 96px;
535 border-radius: 12px;
536 }
537 {$playerId} .fp-audio-layout:not(.fp-audio-no-poster) media-play-button {
538 left: 128px;
539 }
540 {$playerId} .fp-audio-layout:not(.fp-audio-no-poster) .fp-audio-content {
541 padding-left: 82px;
542 }
543 ";
544 }
545
546 $aspectRatio = Arr::get($settings, 'aspectRatio');
547 if ($aspectRatio && $aspectRatio != 'original' && preg_match('/^\d+:\d+$/', $aspectRatio)) {
548 $cssAspectRatio = preg_replace('/^(\d+):(\d+)$/', '$1/$2', $aspectRatio);
549 $customCss .= "
550 #fluent_player_" . esc_attr($instanceId) . " {
551 aspect-ratio: " . esc_attr($cssAspectRatio) . ";
552 }
553 #fluent_player_" . esc_attr($instanceId) . " media-player[data-view-type='video'] {
554 aspect-ratio: " . esc_attr($cssAspectRatio) . ";
555 }
556 ";
557 } else if (!$isAudio) {
558 $customCss .= "
559 #fluent_player_" . esc_attr($instanceId) . " {
560 max-width: 100%;
561 min-height: 300px;
562 }
563
564 @media (max-width: 768px) {
565 #fluent_player_" . esc_attr($instanceId) . " {
566 min-height: 200px;
567 }
568 }
569 ";
570 }
571 if (!empty($customCss)) {
572 return '<style>' . $customCss . '</style>';
573 }
574 return '';
575 }
576
577 private function getFluentplayerDefaultsSettings()
578 {
579 $settings = [
580 'src' => '',
581 'title' => '',
582 'posterSrc' => '',
583 'viewType' => 'video',
584 'brandColor' => '#4a90e2',
585 'aspectRatio' => 'original',
586 'playerWidth' => '',
587 ];
588 $mediaSettings = Bootstrap::getSettings();
589 if (Arr::isTrue($mediaSettings, 'behaviors.muted_autoplay')) {
590 $mediaSettings['autoplay'] = true;
591 $mediaSettings['muted'] = true;
592 }
593 $settings['loadStrategy'] = 'idle'; // for SPA context this needs to be set to 'idle'
594 $settings = array_merge($settings, $mediaSettings);
595
596 // Apply iOS Safari compatibility (playsinline, preload; muted only when autoplay is on)
597 if (class_exists('\FluentPlayer\App\Services\MediaService')) {
598 [$isIosSafari, $iosSafariSettings] = \FluentPlayer\App\Services\MediaService::getIOSSafariSettings($settings);
599 if ($isIosSafari) {
600 $settings = array_merge($settings, $iosSafariSettings);
601 }
602 }
603
604 return apply_filters('fluent_community/fluentplayer_defaults_settings', $settings);
605 }
606 }
607