PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.78
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.78
9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 8.5.4 All 221 releases
wpvr / admin / helpers / class-wpvr-format.php

class-wpvr-format.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 8.5.78, at admin/helpers/class-wpvr-format.php

2,688 lines 114.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) exit; // Exit if accessed directly
3 /**
4 * Responsible for managing WPVR data formation
5 *
6 * @link http://rextheme.com/
7 * @since 1.0.0
8 *
9 * @package Wpvr
10 * @subpackage Wpvr/admin/views
11 */
12
13 class WPVR_Format
14 {
15
16 /**
17 * Setup checkboxes values for meta fields
18 * @param string $arg
19 *
20 * @return bool
21 * @since 8.0.0
22 */
23 public function set_checkbox_value($arg)
24 {
25 $data = sanitize_text_field($arg);
26 if ($data == 'off') {
27 return $data = false;
28 } else {
29 return $data = true;
30 }
31 }
32
33
34 /**
35 * Setup checkboxes values for meta fields while default value is on
36 * @param string $arg
37 *
38 * @return bool
39 * @since 8.0.0
40 */
41 public function set_checkbox_on_value($arg)
42 {
43 $data = sanitize_text_field($arg);
44 if ($data == 'on') {
45 return $data = true;
46 } else {
47 return $data = false;
48 }
49 }
50
51
52 /**
53 * Setup checkboxes values for pro version meta fields
54 * @param string $arg
55 *
56 * @return bool
57 * @since 8.0.0
58 */
59 public function set_pro_checkbox_value($arg)
60 {
61 $data = sanitize_text_field($arg);
62 if ($data == 'on') {
63 return $data = 'on';
64 } else {
65 return $data = 'off';
66 }
67 }
68
69
70 /**
71 * Preapre default scene ID for Tour
72 * @param array $panodata
73 *
74 * @return string
75 * @since 8.0.0
76 */
77 public function prepare_default_scene($panodata)
78 {
79 $allsceneids = array();
80
81 foreach ($panodata["scene-list"] as $panoscenes) {
82 if (!empty($panoscenes['scene-id'])) {
83 array_push($allsceneids, $panoscenes['scene-id']);
84 }
85 if ($panoscenes['dscene'] == 'on') {
86 return $default_scene = $panoscenes['scene-id'];
87 }
88 }
89
90 if (empty($default_scene)) {
91 if ($allsceneids) {
92 return $default_scene = $allsceneids[0];
93 }
94 }
95 }
96
97
98 /**
99 * Prepare panaromic data for Tour
100 *
101 * @param string $panodata
102 *
103 * @return array
104 * @since 8.0.0
105 */
106 public function prepare_panodata($panodata)
107 {
108 $panodata = (array)json_decode($panodata);
109 $panolist = array();
110 if (is_array($panodata["scene-list"])) {
111 foreach ($panodata["scene-list"] as $scenes_data) {
112 $temp_array = array();
113 $temp_array = (array)$scenes_data;
114
115 if ($temp_array['hotspot-list']) {
116 $_hotspot_array = array();
117 foreach ($temp_array['hotspot-list'] as $temp_hotspot) {
118
119 $temp_hotspot = (array)$temp_hotspot;
120 if (isset($temp_hotspot['hotspot-pitch'])) {
121 $temp_hotspot['hotspot-pitch'] = trim($temp_hotspot['hotspot-pitch']);
122 }
123 if (isset($temp_hotspot['hotspot-yaw'])) {
124 $temp_hotspot['hotspot-yaw'] = trim($temp_hotspot['hotspot-yaw']);
125 }
126 $_hotspot_array[] = $temp_hotspot;
127 }
128 }
129
130 $temp_array['hotspot-list'] = $_hotspot_array;
131 $panolist['scene-list'][] = $temp_array;
132 }
133 }
134 $panodata = $panolist;
135 return $panodata;
136 }
137
138
139 /**
140 * Remove empty scene and hotspot from panaromic data list
141 *
142 * @param array $panodata
143 *
144 * @return array
145 * @since 8.0.0
146 */
147 public function remove_empty_scene_and_hotspot($panodata)
148 {
149 $panolength = count($panodata["scene-list"]);
150 for ($i = 0; $i < $panolength; $i++) {
151 if (empty($panodata["scene-list"][$i]['scene-id'])) {
152 unset($panodata["scene-list"][$i]);
153 } else {
154 $panohotspotlength = count($panodata["scene-list"][$i]['hotspot-list']);
155 for ($j = 0; $j < $panohotspotlength; $j++) {
156 if (empty($panodata["scene-list"][$i]['hotspot-list'][$j]['hotspot-title'])) {
157 unset($panodata["scene-list"][$i]['hotspot-list'][$j]);
158 }
159 }
160 }
161 }
162 return $panodata;
163 }
164
165
166 /**
167 * Prepare tour rotation wrapper data
168 *
169 * @param array $pano_array
170 * @param string $rotation
171 *
172 * @return array
173 * @since 8.0.0
174 */
175 public function prepare_rotation_wrapper_data($pano_array, $rotation)
176 {
177 if ($rotation == 'off') {
178 unset($pano_array['autoRotate']);
179 unset($pano_array['autoRotateInactivityDelay']);
180 unset($pano_array['autoRotateStopDelay']);
181 }
182 if (empty($pano_array['autoRotate'])) {
183 unset($pano_array['autoRotate']);
184 unset($pano_array['autoRotateInactivityDelay']);
185 unset($pano_array['autoRotateStopDelay']);
186 }
187 if (empty($pano_array['autoRotateInactivityDelay'])) {
188 unset($pano_array['autoRotateInactivityDelay']);
189 }
190 if (empty($pano_array['autoRotateStopDelay'])) {
191 unset($pano_array['autoRotateStopDelay']);
192 }
193 return $pano_array;
194 }
195
196
197 public function prepare_video_settings_data()
198 {
199 $autoplay = '';
200 if (isset($_POST['autoplay'])) {
201 $autoplay = sanitize_text_field($_POST['autoplay']);
202 }
203
204 $loop = '';
205 if (isset($_POST['loop'])) {
206 $loop = sanitize_text_field($_POST['loop']);
207 }
208
209 return array(
210 'autoplay' => $autoplay,
211 'loop' => $loop
212 );
213 }
214
215
216 /**
217 * Prepare meta data when video url has youtube
218 *
219 * @param string $videourl
220 *
221 * @return string
222 * @since 8.0.0
223 */
224 public function prepare_youtube_video_meta_data($videourl, $videodata)
225 {
226 $explodeid = '';
227 $explodeid = explode("=", $videourl);
228
229 if ($videodata['autoplay'] == 'on') {
230 $autoplay = '&autoplay=1';
231 } else {
232 $autoplay = '';
233 }
234
235 if ($videodata['loop'] == 'on') {
236 $loop = '&loop=1';
237 } else {
238 $loop = '';
239 }
240
241 $foundid = '';
242 $foundid = $explodeid[1] . '?' . $autoplay . $loop;
243 $html = '';
244 $html .= '<iframe width="600" height="400" src="https://www.youtube.com/embed/' . $foundid . '" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
245 return $html;
246 }
247
248 /**
249 * Prepare meta data when video url has youtu.be
250 *
251 * @param string $videourl
252 *
253 * @return string
254 * @since 8.0.0
255 */
256 public function prepare_youtu_be_video_meta_data($videourl, $videodata)
257 {
258 $explodeid = '';
259 $explodeid = explode("/", $videourl);
260
261 if ($videodata['autoplay'] == 'on') {
262 $autoplay = '&autoplay=1';
263 } else {
264 $autoplay = '';
265 }
266
267 if ($videodata['loop'] == 'on') {
268 $loop = '&loop=1';
269 } else {
270 $loop = '';
271 }
272
273 $foundid = '';
274 $foundid = $explodeid[3] . '?' . $autoplay . $loop;
275 $html = '';
276 $html .= '<iframe width="600" height="400" src="https://www.youtube.com/embed/' . $foundid . '" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
277 return $html;
278 }
279
280 /**
281 * Prepare meta data when video url has vimeo
282 *
283 * @param string $videourl
284 *
285 * @return string
286 * @since 8.0.0
287 */
288 public function prepare_vimeo_video_meta_data($videourl, $videodata)
289 {
290 $vid_parts = explode( '/', rtrim( $videourl, '/' ) );
291 $vid_id = end( $vid_parts );
292
293 $do_autoplay = ( $videodata['autoplay'] === 'on' ) ? 'true' : 'false';
294 $do_loop = ( $videodata['loop'] === 'on' ) ? 'true' : 'false';
295
296 $autoplay_param = ( $videodata['autoplay'] === 'on' ) ? 'autoplay=1&muted=1' : '';
297 $loop_param = '';
298
299 $query_parts = array_filter( [ $autoplay_param, $loop_param ] );
300 $foundid = ! empty( $query_parts ) ? $vid_id . '?' . implode( '&', $query_parts ) : $vid_id;
301
302 $iframe_id = 'wpvr-vimeo-preview-' . wp_rand( 10000, 99999 );
303
304 $html = '';
305 $html .= '<iframe id="' . esc_attr( $iframe_id ) . '" src="https://player.vimeo.com/video/' . esc_attr( $foundid ) . '" width="600" height="400" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>';
306 $html .= '<script>
307 (function() {
308 var IFRAME_ID = ' . wp_json_encode( $iframe_id ) . ';
309 var DO_AUTOPLAY = ' . $do_autoplay . ';
310 var DO_LOOP = ' . $do_loop . ';
311
312 function wpvrLoadVimeoSdk( callback ) {
313 if ( window.Vimeo && window.Vimeo.Player ) { callback(); return; }
314 window.wpvrVimeoSdkCallbacks = window.wpvrVimeoSdkCallbacks || [];
315 window.wpvrVimeoSdkCallbacks.push( callback );
316 if ( window.wpvrVimeoSdkLoading ) { return; }
317 window.wpvrVimeoSdkLoading = true;
318 var tag = document.createElement( "script" );
319 tag.src = "https://player.vimeo.com/api/player.js";
320 tag.onload = function() {
321 var cbs = window.wpvrVimeoSdkCallbacks || [];
322 while ( cbs.length ) { ( cbs.shift() )(); }
323 };
324 document.head.appendChild( tag );
325 }
326
327 function initVimeoPlayer() {
328 var iframeEl = document.getElementById( IFRAME_ID );
329 if ( !iframeEl ) { return; }
330 var player = new Vimeo.Player( iframeEl );
331
332 player.ready().then( function() {
333 if ( DO_LOOP ) {
334 player.setLoop( true ).catch( function() {} );
335 }
336 if ( DO_AUTOPLAY ) {
337 player.setVolume( 0 ).then( function() {
338 player.play().catch( function() {} );
339 } ).catch( function() {
340 player.play().catch( function() {} );
341 } );
342 }
343 } );
344 }
345
346 if ( document.readyState === "loading" ) {
347 document.addEventListener( "DOMContentLoaded", function() { wpvrLoadVimeoSdk( initVimeoPlayer ); } );
348 } else {
349 wpvrLoadVimeoSdk( initVimeoPlayer );
350 }
351 }());
352 </script>';
353 return $html;
354 }
355
356 /**
357 * Prepare meta data when video is selfhosted
358 *
359 * @param string $videourl
360 *
361 * @return string
362 * @since 8.0.0
363 */
364 public function prepare_selfhost_video_meta_data($videourl, $vidid, $videodata)
365 {
366 if ($videodata['autoplay'] == 'on') {
367 $autoplay = 'autoplay muted';
368 } else {
369 $autoplay = '';
370 }
371
372 if ($videodata['loop'] == 'on') {
373 $loop = 'loop';
374 } else {
375 $loop = '';
376 }
377
378 $html = '';
379 $html .= '<video id="' . $vidid . '" class="video-js vjs-default-skin vjs-big-play-centered" ' . $autoplay . ' ' . $loop . ' controls preload="auto" style="width:100%; height: 100%;" poster="" >';
380 $html .= '<source src="' . $videourl . '" type="video/mp4"/>';
381 $html .= '<p class="vjs-no-js">';
382 $html .= 'To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com html5-video-support/" target="_blank">supports HTML5 video</a>';
383 $html .= '</p>';
384 $html .= '</video>';
385 return $html;
386 }
387
388
389 /**
390 * Prepare scene data for Tour preview
391 *
392 * @param array $panodata
393 *
394 * @return array
395 * @since 8.0.0
396 */
397 public function prepare_scene_data_for_preview($panodata)
398 {
399 $scene_data = array();
400 foreach ($panodata["scene-list"] as $panoscenes) {
401 if (!empty($panoscenes['scene-id'])) {
402 $scene_ititle = sanitize_text_field(@$panoscenes["scene-ititle"]);
403 $scene_author = sanitize_text_field(@$panoscenes["scene-author"]);
404
405 $scene_vaov = isset($panoscenes["scene-vaov"]) ? (float)$panoscenes["scene-vaov"] : 180;
406 $scene_haov = isset($panoscenes["scene-vaov"]) ? (float)$panoscenes["scene-haov"] : 360;
407 $scene_vertical_offset = isset($panoscenes["scene-vertical-offset"]) ? (float)$panoscenes["scene-vertical-offset"] : 0;
408
409 $default_scene_pitch = isset($panoscenes["scene-pitch"]) ? (float)$panoscenes["scene-pitch"] : null;
410 $default_scene_yaw = isset($panoscenes["scene-yaw"]) ? (float)$panoscenes["scene-yaw"] : null;
411 $scene_max_pitch = isset($panoscenes["scene-maxpitch"]) ? (float)$panoscenes["scene-maxpitch"] : '';
412 $scene_min_pitch = isset($panoscenes["scene-minpitch"]) ? (float)$panoscenes["scene-minpitch"] : '';
413 $scene_max_yaw = isset($panoscenes["scene-maxyaw"]) ? (float)$panoscenes["scene-maxyaw"] : '';
414 $scene_min_yaw = isset($panoscenes["scene-minyaw"]) ? (float)$panoscenes["scene-minyaw"] : '';
415
416 $default_zoom = isset($panoscenes["scene-zoom"]) ? $panoscenes["scene-zoom"] : 100;
417 if (!empty($default_zoom)) {
418 $default_zoom = isset($panoscenes["scene-zoom"]) ? (int)$panoscenes["scene-zoom"] : (int)$default_zoom;
419 } else {
420 $default_zoom = 100;
421 }
422
423 $max_zoom = isset($panoscenes["scene-maxzoom"]) ? $panoscenes["scene-maxzoom"] : 120;
424 if (!empty($max_zoom)) {
425 $max_zoom = isset($panoscenes["scene-maxzoom"]) ? (int)$panoscenes["scene-maxzoom"] : (int)$max_zoom;
426 } else {
427 $max_zoom = 120;
428 }
429
430 $min_zoom = isset($panoscenes["scene-minzoom"]) ? $panoscenes["scene-minzoom"] : 50;
431 if (!empty($min_zoom)) {
432 $min_zoom = isset($panoscenes["scene-minzoom"]) ? (int)$panoscenes["scene-minzoom"] : (int)$min_zoom;
433 } else {
434 $min_zoom = 50;
435 }
436
437 $hotspot_datas = $panoscenes["hotspot-list"];
438
439 $hotspots = $this->prepare_hotspot_data_for_preview($hotspot_datas);
440
441 $scene_info = array();
442 if ($panoscenes["scene-type"] == 'cubemap') {
443 $pano_type = 'cubemap';
444 $pano_attachment = array(
445 $panoscenes["scene-attachment-url-face0"],
446 $panoscenes["scene-attachment-url-face1"],
447 $panoscenes["scene-attachment-url-face2"],
448 $panoscenes["scene-attachment-url-face3"],
449 $panoscenes["scene-attachment-url-face4"],
450 $panoscenes["scene-attachment-url-face5"]
451 );
452
453 $scene_info = array("type" => $panoscenes["scene-type"], "cubeMap" => $pano_attachment, "pitch" => $default_scene_pitch, "maxPitch" => $scene_max_pitch, "minPitch" => $scene_min_pitch, "maxYaw" => $scene_max_yaw, "minYaw" => $scene_min_yaw, "yaw" => $default_scene_yaw, "hfov" => $default_zoom, "maxHfov" => $max_zoom, "minHfov" => $min_zoom, "title" => $scene_ititle, "author" => $scene_author, "vaov" => $scene_vaov, "haov" => $scene_haov, "vOffset" => $scene_vertical_offset, "hotSpots" => $hotspots);
454 } else {
455 $scene_info = array("type" => $panoscenes["scene-type"], "panorama" => $panoscenes["scene-attachment-url"], "pitch" => $default_scene_pitch, "maxPitch" => $scene_max_pitch, "minPitch" => $scene_min_pitch, "maxYaw" => $scene_max_yaw, "minYaw" => $scene_min_yaw, "yaw" => $default_scene_yaw, "hfov" => $default_zoom, "maxHfov" => $max_zoom, "minHfov" => $min_zoom, "title" => $scene_ititle, "author" => $scene_author, "vaov" => $scene_vaov, "haov" => $scene_haov, "vOffset" => $scene_vertical_offset, "hotSpots" => $hotspots);
456 }
457
458 if (empty($panoscenes["scene-ititle"])) {
459 unset($scene_info['title']);
460 }
461 if (empty($panoscenes["scene-author"])) {
462 unset($scene_info['author']);
463 }
464
465 if (empty($scene_vaov)) {
466 unset($scene_info['vaov']);
467 }
468
469 if (empty($scene_haov)) {
470 unset($scene_info['haov']);
471 }
472
473 if (empty($scene_vertical_offset)) {
474 unset($scene_info['vOffset']);
475 }
476
477 if (isset($panoscenes["cvgscene"])) {
478 if ($panoscenes["cvgscene"] == "off") {
479 unset($scene_info['maxPitch']);
480 unset($scene_info['minPitch']);
481 }
482 }
483
484 if (empty($panoscenes["scene-maxpitch"])) {
485 unset($scene_info['maxPitch']);
486 }
487
488 if (empty($panoscenes["scene-minpitch"])) {
489 unset($scene_info['minPitch']);
490 }
491
492 if (isset($panoscenes["chgscene"])) {
493 if ($panoscenes["chgscene"] == "off") {
494 unset($scene_info['maxYaw']);
495 unset($scene_info['minYaw']);
496 }
497 }
498 if (empty($panoscenes["scene-maxyaw"])) {
499 unset($scene_info['maxYaw']);
500 }
501
502 if (empty($panoscenes["scene-minyaw"])) {
503 unset($scene_info['minYaw']);
504 }
505
506 $scene_array = array();
507 $scene_array = array(
508 $panoscenes["scene-id"] => $scene_info
509 );
510 $scene_data[$panoscenes["scene-id"]] = $scene_info;
511 }
512 }
513 return $scene_data;
514 }
515
516
517 /**
518 * Preapre hotspot data for Tour preview
519 *
520 * @param array $hotspot_datas
521 *
522 * @return array
523 * @since 8.0.0
524 */
525 private function prepare_hotspot_data_for_preview($hotspot_datas)
526 {
527 $hotspots = array();
528 foreach ($hotspot_datas as $hotspot_data) {
529
530 if (!empty($hotspot_data["hotspot-title"])) {
531
532 $hotspot_type = $hotspot_data["hotspot-type"] !== 'scene' ? 'info' : $hotspot_data["hotspot-type"];
533 $hotspot_content = '';
534
535 ob_start();
536 do_action('wpvr_hotspot_content_admin', $hotspot_data);
537 $hotspot_content = ob_get_clean();
538
539
540 if (!$hotspot_content) $hotspot_content = $hotspot_data["hotspot-content"];
541
542
543 $hotspot_info = array(
544 "text" => $hotspot_data["hotspot-title"],
545 "pitch" => $hotspot_data["hotspot-pitch"],
546 "yaw" => $hotspot_data["hotspot-yaw"],
547 "type" => $hotspot_type,
548 "URL" => $hotspot_data["hotspot-url"],
549 "clickHandlerArgs" => $hotspot_content,
550 "createTooltipArgs" => $hotspot_data["hotspot-hover"],
551 "sceneId" => $hotspot_data["hotspot-scene"],
552 'hotspot_type' => $hotspot_data['hotspot-type']
553 );
554
555 array_push($hotspots, $hotspot_info);
556 if (empty($hotspot_data["hotspot-scene"])) {
557 unset($hotspot_info['targetPitch']);
558 unset($hotspot_info['targetYaw']);
559 }
560 }
561 }
562 return $hotspots;
563 }
564
565
566 /**
567 * Prepare youtube video for preview
568 *
569 * @param string $videourl
570 *
571 * @return string
572 * @since 8.0.0
573 */
574 public function prepare_youtube_video_preview($videourl, $videodata)
575 {
576 $muted = ($videodata['autoplay'] == 'on') ? '&mute=1' : '';
577 $autoplay = ($videodata['autoplay'] == 'on') ? '&autoplay=1' : '';
578 $loop = ($videodata['loop'] == 'on') ? '&loop=1' : '';
579 $origin = '&origin=' . rawurlencode( home_url() );
580
581 $expdata = $this->extract_youtube_video_id( $videourl );
582 $playlist = '&playlist=' . $expdata;
583
584 $preview_id = 'wpvr-preview-' . wp_rand(10000, 99999);
585
586 $html = '';
587 $html .= '<div id="' . esc_attr( $preview_id . '-container' ) . '" style="position:relative; width:100%; max-width:523px; height:400px;">';
588
589 // Iframe
590 $html .= '<div id="' . esc_attr( $preview_id . '-frame' ) . '" style="width:100%; height:100%;">';
591 $html .= '<iframe id="' . esc_attr( $preview_id . '-iframe' ) . '" src="https://www.youtube.com/embed/' . esc_attr( $expdata ) . '?rel=0&modestbranding=1' . $loop . '&autohide=1' . $muted . '&showinfo=0&controls=0' . $autoplay . $playlist . '&enablejsapi=1&playsinline=1&html5=1' . $origin . '" width="100%" height="100%" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; xr-spatial-tracking"></iframe>';
592 $html .= '</div>';
593
594 // Drag surface
595 $html .= '<div id="' . esc_attr( $preview_id . '-drag-surface' ) . '" style="position:absolute; top:0; left:0; right:0; bottom:48px; display:none; pointer-events:none; cursor:grab; z-index:3; background:transparent;"></div>';
596
597 // Custom control bar
598 $html .= '<div id="' . esc_attr( $preview_id . '-controls' ) . '" style="display:none; position:absolute; bottom:0; left:0; right:0; height:48px; z-index:5; pointer-events:auto; background:linear-gradient(transparent, rgba(0,0,0,0.7)); align-items:center; padding:0 8px; gap:6px; box-sizing:border-box;">';
599
600 // Play/Pause
601 $html .= '<button type="button" id="' . esc_attr( $preview_id . '-play-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0;" title="Play">';
602 $html .= '<svg id="' . esc_attr( $preview_id . '-play-icon' ) . '" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M8 5v14l11-7z"/></svg>';
603 $html .= '</button>';
604
605 // Time current
606 $html .= '<span id="' . esc_attr( $preview_id . '-time-current' ) . '" style="color:#fff; font-size:12px; font-family:Arial,sans-serif; min-width:36px; text-align:center; user-select:none;">0:00</span>';
607
608 // Progress bar
609 $html .= '<div id="' . esc_attr( $preview_id . '-progress-wrap' ) . '" style="flex:1; height:4px; background:rgba(255,255,255,0.3); border-radius:2px; cursor:pointer; position:relative;">';
610 $html .= '<div id="' . esc_attr( $preview_id . '-progress-buffered' ) . '" style="position:absolute; top:0; left:0; height:100%; background:rgba(255,255,255,0.4); border-radius:2px; pointer-events:none;"></div>';
611 $html .= '<div id="' . esc_attr( $preview_id . '-progress-bar' ) . '" style="position:absolute; top:0; left:0; height:100%; background:#ff0000; border-radius:2px; pointer-events:none;"></div>';
612 $html .= '<div id="' . esc_attr( $preview_id . '-progress-thumb' ) . '" style="position:absolute; top:50%; left:0; width:12px; height:12px; background:#ff0000; border-radius:50%; transform:translate(-50%,-50%); pointer-events:none; display:none;"></div>';
613 $html .= '</div>';
614
615 // Time duration
616 $html .= '<span id="' . esc_attr( $preview_id . '-time-duration' ) . '" style="color:#fff; font-size:12px; font-family:Arial,sans-serif; min-width:36px; text-align:center; user-select:none;">0:00</span>';
617
618 // Mute button
619 $html .= '<button id="' . esc_attr( $preview_id . '-mute-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0; position:relative;" title="Mute">';
620 $html .= '<svg id="' . esc_attr( $preview_id . '-vol-icon' ) . '" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>';
621 $html .= '</button>';
622
623 // Volume slider
624 $html .= '<style>#' . esc_attr( $preview_id ) . '-vol-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:12px;height:12px;border-radius:50%;background:#fff;cursor:pointer;margin-top:-4.5px;}#' . esc_attr( $preview_id ) . '-vol-slider::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#fff;border:none;cursor:pointer;}#' . esc_attr( $preview_id ) . '-vol-slider::-webkit-slider-runnable-track{height:3px;border-radius:1.5px;}#' . esc_attr( $preview_id ) . '-vol-slider::-moz-range-track{height:3px;border-radius:1.5px;background:rgba(255,255,255,0.3);}</style>';
625 $html .= '<div id="' . esc_attr( $preview_id . '-vol-slider-wrap' ) . '" style="display:none; align-items:center; height:36px; padding:0 4px;">';
626 $html .= '<input id="' . esc_attr( $preview_id . '-vol-slider' ) . '" type="range" min="0" max="100" value="100" style="width:52px; height:3px; cursor:pointer; -webkit-appearance:none; appearance:none; background:linear-gradient(to right,#fff 100%,rgba(255,255,255,0.3) 100%); border-radius:1.5px; outline:none;" />';
627 $html .= '</div>';
628
629 // Fullscreen button
630 $html .= '<button id="' . esc_attr( $preview_id . '-fs-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0;" title="Toggle fullscreen">';
631 $html .= '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
632 $html .= '</button>';
633
634 $html .= '</div>'; // Close controls
635 $html .= '</div>'; // Close container
636
637 // Script
638 $html .= '<script>
639 (function() {
640 var playerInitialised = false;
641 var ytPlayer = null;
642 var dragSurface = document.getElementById("' . esc_js( $preview_id ) . '-drag-surface");
643 var isDragging = false;
644 var dragStartX = 0;
645 var dragStartY = 0;
646 var dragStartView = null;
647
648 function clamp(value, min, max) {
649 return Math.min(Math.max(value, min), max);
650 }
651
652 function getSphericalView() {
653 if (!ytPlayer || typeof ytPlayer.getSphericalProperties !== "function") return null;
654 var view = ytPlayer.getSphericalProperties();
655 return view && Object.keys(view).length ? view : null;
656 }
657
658 function setDragSurfaceEnabled(enabled) {
659 if (!dragSurface) return;
660 dragSurface.style.display = enabled ? "block" : "none";
661 dragSurface.style.pointerEvents = enabled ? "auto" : "none";
662 }
663
664 function getEventPoint(event) {
665 if (event.touches && event.touches.length) {
666 return event.touches[0];
667 }
668
669 if (event.changedTouches && event.changedTouches.length) {
670 return event.changedTouches[0];
671 }
672
673 return event;
674 }
675
676 function beginDrag(event) {
677 var point = getEventPoint(event);
678 var sphericalView = getSphericalView();
679
680 if (!sphericalView || !point) return;
681
682 event.preventDefault();
683 dragStartX = point.clientX;
684 dragStartY = point.clientY;
685 dragStartView = sphericalView;
686 dragSurface.style.cursor = "grabbing";
687 }
688
689 function updateDrag(event) {
690 var deltaX;
691 var deltaY;
692 var nextView;
693 var point = getEventPoint(event);
694
695 if (!point) {
696 return;
697 }
698
699 if (!isDragging && dragStartView) {
700 deltaX = point.clientX - dragStartX;
701 deltaY = point.clientY - dragStartY;
702 if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 5) isDragging = true;
703 }
704
705 if (!isDragging || !dragStartView || !ytPlayer || typeof ytPlayer.setSphericalProperties !== "function") return;
706
707 event.preventDefault();
708 deltaX = point.clientX - dragStartX;
709 deltaY = point.clientY - dragStartY;
710 nextView = {
711 yaw: (dragStartView.yaw || 0) + (deltaX * 0.2),
712 pitch: clamp((dragStartView.pitch || 0) + (deltaY * 0.2), -85, 85),
713 roll: dragStartView.roll || 0,
714 fov: dragStartView.fov || 100
715 };
716 ytPlayer.setSphericalProperties(nextView);
717 }
718
719 function endDrag() {
720 if (!isDragging && dragStartView && ytPlayer) {
721 var playerState = ytPlayer.getPlayerState();
722 if (playerState === 1) ytPlayer.pauseVideo();
723 else ytPlayer.playVideo();
724 }
725 isDragging = false;
726 dragStartView = null;
727 if (dragSurface) dragSurface.style.cursor = "grab";
728 }
729
730 function update360DragAvailability() {
731 var sphericalView = getSphericalView();
732 setDragSurfaceEnabled(!!sphericalView);
733 }
734
735 function schedule360AvailabilityChecks() {
736 update360DragAvailability();
737 window.setTimeout(update360DragAvailability, 500);
738 window.setTimeout(update360DragAvailability, 1500);
739 window.setTimeout(update360DragAvailability, 3000);
740 }
741
742 if (dragSurface) {
743 dragSurface.addEventListener("mousedown", function(event) {
744 beginDrag(event);
745 });
746
747 dragSurface.addEventListener("touchstart", function(event) {
748 beginDrag(event);
749 }, { passive: false });
750
751 dragSurface.addEventListener("dragstart", function(event) { event.preventDefault(); });
752
753 window.addEventListener("mousemove", function(event) {
754 updateDrag(event);
755 });
756
757 window.addEventListener("touchmove", function(event) {
758 updateDrag(event);
759 }, { passive: false });
760
761 window.addEventListener("mouseup", function() {
762 endDrag();
763 });
764
765 window.addEventListener("touchend", function() {
766 endDrag();
767 });
768
769 window.addEventListener("touchcancel", function() {
770 endDrag();
771 });
772 }
773
774 function wpvrLoadYouTubeApi(callback) {
775 window.wpvrYoutubeApiCallbacks = window.wpvrYoutubeApiCallbacks || [];
776 if (window.YT && window.YT.Player) { callback(); return; }
777 window.wpvrYoutubeApiCallbacks.push(callback);
778 if (window.wpvrYoutubeApiLoading) return;
779 window.wpvrYoutubeApiLoading = true;
780 var previousReady = window.onYouTubeIframeAPIReady;
781 window.onYouTubeIframeAPIReady = function() {
782 if (typeof previousReady === "function") previousReady();
783 var callbacks = window.wpvrYoutubeApiCallbacks || [];
784 while (callbacks.length) {
785 var queuedCallback = callbacks.shift();
786 if (typeof queuedCallback === "function") queuedCallback();
787 }
788 };
789 var tag = document.createElement("script");
790 tag.src = "https://www.youtube.com/iframe_api";
791 var firstScriptTag = document.getElementsByTagName("script")[0];
792 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
793 }
794
795 function initializeYouTubePlayer() {
796 if (playerInitialised) return;
797 wpvrLoadYouTubeApi(function() {
798 if (playerInitialised || !(window.YT && window.YT.Player)) return;
799 ytPlayer = new YT.Player("' . esc_js( $preview_id . '-iframe' ) . '", {
800 events: {
801 onReady: function() {
802 schedule360AvailabilityChecks();
803 startProgressLoop();
804 },
805 onStateChange: function() {
806 schedule360AvailabilityChecks();
807 startProgressLoop();
808 }
809 }
810 });
811 playerInitialised = true;
812 });
813 }
814
815 // === Custom control bar logic ===
816 var outerContainer = document.getElementById("' . esc_js( $preview_id ) . '-container");
817 var controlBar = document.getElementById("' . esc_js( $preview_id ) . '-controls");
818 var playBtn = document.getElementById("' . esc_js( $preview_id ) . '-play-btn");
819 var playIcon = document.getElementById("' . esc_js( $preview_id ) . '-play-icon");
820 var timeCurrent = document.getElementById("' . esc_js( $preview_id ) . '-time-current");
821 var timeDuration = document.getElementById("' . esc_js( $preview_id ) . '-time-duration");
822 var progressWrap = document.getElementById("' . esc_js( $preview_id ) . '-progress-wrap");
823 var progressBar = document.getElementById("' . esc_js( $preview_id ) . '-progress-bar");
824 var progressBuffered = document.getElementById("' . esc_js( $preview_id ) . '-progress-buffered");
825 var progressThumb = document.getElementById("' . esc_js( $preview_id ) . '-progress-thumb");
826 var fsBtn = document.getElementById("' . esc_js( $preview_id ) . '-fs-btn");
827 var frameEl = document.getElementById("' . esc_js( $preview_id ) . '-frame");
828 var muteBtn = document.getElementById("' . esc_js( $preview_id ) . '-mute-btn");
829 var volSlider = document.getElementById("' . esc_js( $preview_id ) . '-vol-slider");
830 var volSliderWrap = document.getElementById("' . esc_js( $preview_id ) . '-vol-slider-wrap");
831 var volIcon = document.getElementById("' . esc_js( $preview_id ) . '-vol-icon");
832
833 var playPath = "M8 5v14l11-7z";
834 var pausePath = "M6 19h4V5H6v14zm8-14v14h4V5h-4z";
835 var volIconMuted = "M16.5 12A4.5 4.5 0 0014 8.14v2.07l2.45 2.45c.03-.21.05-.43.05-.66zm2.5 0c0 .93-.21 1.82-.58 2.61l1.47 1.47A8.94 8.94 0 0021 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 003.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z";
836 var volIconLow = "M7 9v6h4l5 5V4L7 9z";
837 var volIconMed = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86z";
838 var volIconHigh = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z";
839
840 function formatTime(sec) {
841 if (!sec || isNaN(sec)) return "0:00";
842 sec = Math.floor(sec);
843 var m = Math.floor(sec / 60);
844 var s = sec % 60;
845 return m + ":" + (s < 10 ? "0" : "") + s;
846 }
847
848 if (controlBar) controlBar.style.display = "flex";
849
850 // --- Play / Pause ---
851 if (playBtn) {
852 playBtn.addEventListener("click", function() {
853 if (!ytPlayer) return;
854 var state = ytPlayer.getPlayerState();
855 if (state === 1) ytPlayer.pauseVideo();
856 else ytPlayer.playVideo();
857 });
858 }
859
860 function updatePlayIcon() {
861 if (!ytPlayer || !playIcon) return;
862 try {
863 var state = ytPlayer.getPlayerState();
864 playIcon.querySelector("path").setAttribute("d", state === 1 ? pausePath : playPath);
865 } catch(e) {}
866 }
867
868 // --- Progress bar ---
869 var progressAnimId = null;
870 var isSeeking = false;
871 var progressLoopStarted = false;
872
873 function startProgressLoop() {
874 if (progressLoopStarted) return;
875 progressLoopStarted = true;
876 progressAnimId = requestAnimationFrame(updateProgress);
877 }
878
879 function updateProgress() {
880 if (!ytPlayer || isSeeking) { progressAnimId = requestAnimationFrame(updateProgress); return; }
881 try {
882 var current = typeof ytPlayer.getCurrentTime === "function" ? ytPlayer.getCurrentTime() : 0;
883 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
884 if (duration > 0) {
885 var pct = (current / duration) * 100;
886 progressBar.style.width = pct + "%";
887 progressThumb.style.left = pct + "%";
888 timeCurrent.textContent = formatTime(current);
889 timeDuration.textContent = formatTime(duration);
890 }
891 if (typeof ytPlayer.getVideoLoadedFraction === "function") {
892 progressBuffered.style.width = (ytPlayer.getVideoLoadedFraction() * 100) + "%";
893 }
894 updatePlayIcon();
895 } catch (e) {}
896 progressAnimId = requestAnimationFrame(updateProgress);
897 }
898
899 if (progressWrap) {
900 function seekFromEvent(e) {
901 var rect = progressWrap.getBoundingClientRect();
902 var x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
903 var pct = x / rect.width;
904 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
905 if (duration > 0 && ytPlayer) {
906 ytPlayer.seekTo(pct * duration, true);
907 progressBar.style.width = (pct * 100) + "%";
908 progressThumb.style.left = (pct * 100) + "%";
909 timeCurrent.textContent = formatTime(pct * duration);
910 }
911 }
912 progressWrap.addEventListener("mousedown", function(e) {
913 isSeeking = true;
914 seekFromEvent(e);
915 function onMove(e) { seekFromEvent(e); }
916 function onUp() {
917 isSeeking = false;
918 document.removeEventListener("mousemove", onMove);
919 document.removeEventListener("mouseup", onUp);
920 }
921 document.addEventListener("mousemove", onMove);
922 document.addEventListener("mouseup", onUp);
923 });
924 progressWrap.addEventListener("mouseenter", function() { progressThumb.style.display = "block"; });
925 progressWrap.addEventListener("mouseleave", function() { if (!isSeeking) progressThumb.style.display = "none"; });
926 }
927
928 // --- Volume ---
929 var volHideTimer = null;
930 var localVol = 100;
931 var localMuted = false;
932
933 function updateVolIconFromLocal() {
934 if (!volIcon) return;
935 var path;
936 if (localMuted || localVol === 0) path = volIconMuted;
937 else if (localVol < 40) path = volIconLow;
938 else if (localVol < 75) path = volIconMed;
939 else path = volIconHigh;
940 volIcon.querySelector("path").setAttribute("d", path);
941 }
942
943 function updateVolIcon() { updateVolIconFromLocal(); }
944
945 function showVolSlider() {
946 if (volHideTimer) { clearTimeout(volHideTimer); volHideTimer = null; }
947 if (volSliderWrap) volSliderWrap.style.display = "flex";
948 }
949 function hideVolSlider() {
950 volHideTimer = setTimeout(function() { if (volSliderWrap) volSliderWrap.style.display = "none"; }, 400);
951 }
952
953 if (muteBtn) {
954 muteBtn.addEventListener("click", function() {
955 if (!ytPlayer) return;
956 if (localMuted) {
957 ytPlayer.unMute(); localMuted = false;
958 if (localVol === 0) { localVol = 100; ytPlayer.setVolume(100); }
959 if (volSlider) volSlider.value = localVol;
960 } else {
961 ytPlayer.mute(); localMuted = true;
962 if (volSlider) volSlider.value = 0;
963 }
964 updateVolIconFromLocal();
965 updateVolSliderFill();
966 });
967 muteBtn.addEventListener("mouseenter", showVolSlider);
968 muteBtn.addEventListener("mouseleave", hideVolSlider);
969 }
970 if (volSliderWrap) {
971 volSliderWrap.addEventListener("mouseenter", showVolSlider);
972 volSliderWrap.addEventListener("mouseleave", hideVolSlider);
973 }
974 function updateVolSliderFill() {
975 if (!volSlider) return;
976 var v = parseInt(volSlider.value, 10);
977 volSlider.style.background = "linear-gradient(to right,#fff " + v + "%,rgba(255,255,255,0.3) " + v + "%)";
978 }
979 if (volSlider) {
980 volSlider.addEventListener("input", function() {
981 if (!ytPlayer) return;
982 var val = parseInt(volSlider.value, 10);
983 localVol = val;
984 ytPlayer.setVolume(val);
985 if (val === 0) { ytPlayer.mute(); localMuted = true; }
986 else if (localMuted) { ytPlayer.unMute(); localMuted = false; }
987 updateVolIconFromLocal();
988 updateVolSliderFill();
989 });
990 updateVolSliderFill();
991 }
992
993 // --- Fullscreen ---
994 if (outerContainer) {
995 var savedFrameWidth = frameEl ? frameEl.style.width : "";
996 var savedFrameHeight = frameEl ? frameEl.style.height : "";
997 var savedFrameMaxWidth = frameEl ? frameEl.style.maxWidth : "";
998 var savedContainerWidth = outerContainer.style.width;
999 var savedContainerHeight = outerContainer.style.height;
1000 var savedContainerBackground = outerContainer.style.background;
1001
1002 if(fsBtn){
1003 fsBtn.addEventListener("click", function() {
1004 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1005 if (fsEl) {
1006 (document.exitFullscreen || document.webkitExitFullscreen).call(document);
1007 } else {
1008 (outerContainer.requestFullscreen || outerContainer.webkitRequestFullscreen).call(outerContainer);
1009 }
1010 });
1011 }
1012
1013 function onFsChange() {
1014 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1015 var isFs = (fsEl === outerContainer);
1016 frameEl.style.width = isFs ? "100%" : savedFrameWidth;
1017 frameEl.style.height = isFs ? "100%" : savedFrameHeight;
1018 frameEl.style.maxWidth = isFs ? "100%" : savedFrameMaxWidth;
1019 outerContainer.style.width = isFs ? "100%" : savedContainerWidth;
1020 outerContainer.style.height = isFs ? "100%" : savedContainerHeight;
1021 outerContainer.style.background = isFs ? "#000" : savedContainerBackground;
1022 fsBtn.querySelector("svg path").setAttribute("d", isFs
1023 ? "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
1024 : "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
1025 );
1026 schedule360AvailabilityChecks();
1027 }
1028 document.addEventListener("fullscreenchange", onFsChange);
1029 document.addEventListener("webkitfullscreenchange", onFsChange);
1030 }
1031
1032 // Sync controls after player ready
1033 var origSchedule = schedule360AvailabilityChecks;
1034 schedule360AvailabilityChecks = function() {
1035 origSchedule();
1036 updateVolIcon();
1037 updatePlayIcon();
1038 };
1039
1040 // Initialize immediately for admin preview
1041 initializeYouTubePlayer();
1042 })();
1043 </script>';
1044
1045 return $html;
1046 }
1047
1048
1049 /**
1050 * Extract YouTube video ID from various URL formats.
1051 *
1052 * Handles youtube.com/watch?v=, youtu.be/, youtube.com/embed/, and youtube.com/v/ formats.
1053 * Strips extra query parameters and sanitizes the ID to only valid YouTube characters.
1054 *
1055 * @param string $url The YouTube video URL.
1056 *
1057 * @return string The sanitized YouTube video ID, or empty string on failure.
1058 * @since 8.0.0
1059 */
1060 private function extract_youtube_video_id( $url ) {
1061 $url = trim( $url );
1062 $parsed = wp_parse_url( $url );
1063
1064 // Handle youtu.be short URLs.
1065 if ( isset( $parsed['host'] ) && 'youtu.be' === $parsed['host'] ) {
1066 $video_id = isset( $parsed['path'] ) ? ltrim( $parsed['path'], '/' ) : '';
1067 return preg_replace( '/[^A-Za-z0-9_\-]/', '', $video_id );
1068 }
1069
1070 // Handle youtube.com/embed/VIDEO_ID or youtube.com/v/VIDEO_ID.
1071 if ( isset( $parsed['path'] ) && preg_match( '#/(embed|v)/([A-Za-z0-9_\-]+)#', $parsed['path'], $matches ) ) {
1072 return $matches[2];
1073 }
1074
1075 // Handle youtube.com/watch?v=VIDEO_ID (standard format).
1076 if ( isset( $parsed['query'] ) ) {
1077 parse_str( $parsed['query'], $query_params );
1078 if ( ! empty( $query_params['v'] ) ) {
1079 return preg_replace( '/[^A-Za-z0-9_\-]/', '', $query_params['v'] );
1080 }
1081 }
1082
1083 // Fallback: try naive extraction for edge cases.
1084 if ( preg_match( '/[?&]v=([A-Za-z0-9_\-]+)/', $url, $matches ) ) {
1085 return $matches[1];
1086 }
1087
1088 return '';
1089 }
1090
1091
1092 /**
1093 * Prepare shortcode data for youtube video url
1094 *
1095 * @param array $postdata
1096 * @param mixed $width
1097 * @param mixed $height
1098 *
1099 * @return string
1100 * @since 8.0.0
1101 */
1102 public function prepare_youtube_video_shortcode_data($postdata, $width, $height, $autoplay, $loop, $radius)
1103 {
1104 $muted = ($autoplay == 'on') ? '&mute=1' : '';
1105 $autoplay = ($autoplay == 'on') ? '&autoplay=1' : '';
1106 $loop = ($loop == 'on') ? '&loop=1' : '';
1107 $origin = '&origin=' . rawurlencode( home_url() );
1108
1109 $expdata = $this->extract_youtube_video_id( $postdata['vidurl'] );
1110
1111 $playlist = '&playlist=' . $expdata;
1112
1113 $html = '';
1114 $html .= '<div style="text-align:center; max-width:100%; width:' . $width . '; height:' . $height . '; border-radius: ' . $radius . '; margin: 0 auto;">';
1115
1116 // Add browser compatibility detection script
1117 $html .= '<script>
1118 function wpvr_check_360_support() {
1119 var support = {
1120 supported: false,
1121 fullySupported: false,
1122 webgl: false,
1123 orientation: false,
1124 gyroscope: false,
1125 touch: false,
1126 browser: "unknown",
1127 isMobile: false,
1128 isIPhone: false,
1129 isAndroid: false,
1130 details: []
1131 };
1132
1133 // Detect mobile devices
1134 support.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
1135 support.isIPhone = /iPhone/i.test(navigator.userAgent);
1136 support.isAndroid = /Android/i.test(navigator.userAgent);
1137 if (support.isMobile) {
1138 support.details.push("Mobile device detected");
1139 }
1140 if (support.isIPhone) {
1141 support.details.push("iPhone detected");
1142 }
1143 if (support.isAndroid) {
1144 support.details.push("Android device detected");
1145 }
1146
1147 // Browser detection
1148 var ua = navigator.userAgent;
1149 if (/^((?!chrome|android).)*safari/i.test(ua)) {
1150 support.browser = "safari";
1151 support.details.push("Safari browser detected");
1152 } else if (ua.indexOf("Chrome") > -1) {
1153 support.browser = "chrome";
1154 support.details.push("Chrome browser detected");
1155 } else if (ua.indexOf("Firefox") > -1) {
1156 support.browser = "firefox";
1157 support.details.push("Firefox browser detected");
1158 } else if (ua.indexOf("MSIE") > -1 || ua.indexOf("Trident") > -1) {
1159 support.browser = "ie";
1160 support.details.push("Internet Explorer detected");
1161 } else if (ua.indexOf("Edge") > -1 || ua.indexOf("Edg") > -1) {
1162 support.browser = "edge";
1163 support.details.push("Edge browser detected");
1164 } else if (ua.indexOf("Opera") > -1 || ua.indexOf("OPR") > -1) {
1165 support.browser = "opera";
1166 support.details.push("Opera browser detected");
1167 }
1168
1169 // Check WebGL support
1170 try {
1171 var canvas = document.createElement("canvas");
1172 support.webgl = !!(window.WebGLRenderingContext &&
1173 (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")));
1174
1175 if (support.webgl) {
1176 support.details.push("WebGL supported");
1177 } else {
1178 support.details.push("WebGL not supported");
1179 }
1180 } catch (e) {
1181 support.webgl = false;
1182 support.details.push("WebGL detection error: " + e.message);
1183 }
1184
1185 // Check device orientation support
1186 support.orientation = !!(window.DeviceOrientationEvent);
1187 if (support.orientation) {
1188 support.details.push("Device Orientation API supported");
1189 } else {
1190 support.details.push("Device Orientation API not supported");
1191 }
1192
1193 // Check for gyroscope
1194 if (window.Gyroscope) {
1195 support.gyroscope = true;
1196 support.details.push("Gyroscope supported");
1197 } else {
1198 support.details.push("Gyroscope not supported");
1199 }
1200
1201 // Check touch support for mobile interaction
1202 support.touch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
1203 if (support.touch) {
1204 support.details.push("Touch supported");
1205 } else {
1206 support.details.push("Touch not supported");
1207 }
1208
1209 // Overall support determination
1210 support.supported = support.webgl;
1211
1212 // Full support means WebGL + orientation on mobile or WebGL on desktop
1213 support.fullySupported = support.webgl;
1214
1215 // Browser-specific warnings
1216 if (support.browser === "ie") {
1217 support.browserWarning = "Internet Explorer has limited support for 360 videos. The experience may not be optimal.";
1218 support.fullySupported = false;
1219 } else if (!support.supported) {
1220 support.browserWarning = "Your browser does not fully support 360° videos. For the best experience, please use a modern browser with WebGL support.";
1221 }
1222
1223 return support;
1224 }
1225 </script>';
1226
1227 $random_id = 'video-container-' . wp_rand(10000, 99999);
1228 $html .= '<div id="' . $random_id . '-container" style="position:relative; width:100%; height:100%;">';
1229
1230 // First, always check browser compatibility before showing anything
1231 $html .= '<div id="' . $random_id . '-compatibility-check" style="position:absolute; top:0; left:0; width:100%; height:100%; display:none; pointer-events:none; flex-direction:column; justify-content:center; align-items:center; background-color:#f9f9f9; border-radius:' . $radius . ';">
1232 <div style="margin-bottom:20px; text-align:center;">
1233 <div class="wpvr-loading-spinner" style="border:5px solid #f3f3f3; border-top:5px solid #3498db; border-radius:50%; width:50px; height:50px; margin:0 auto 15px; animation:wpvr-spin 1s linear infinite;"></div>
1234 <p style="margin:0;">Checking browser compatibility...</p>
1235 </div>
1236 </div>';
1237
1238 // Add animation for spinner
1239 $html .= '<style>@keyframes wpvr-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>';
1240
1241 // Add the iframe (visible by default so YouTube 360° WebGL context gets correct dimensions)
1242 $html .= '<div id="' . esc_attr( $random_id . '-frame' ) . '" style="text-align:center; max-width:100%; width:' . esc_attr( $width ) . '; height:' . esc_attr( $height ) . '; border-radius:' . esc_attr( $radius ) . '; margin: 0 auto; display:block;">';
1243 $html .= '<iframe id="' . esc_attr( $random_id . '-iframe' ) . '" src="https://www.youtube.com/embed/' . esc_attr( $expdata ) . '?rel=0&modestbranding=1' . $loop . '&autohide=1' . $muted . '&showinfo=0&controls=0' . $autoplay . $playlist . '&enablejsapi=1&playsinline=1&html5=1' . $origin . '" width="100%" height="100%" style="border-radius:' . esc_attr( $radius ) . ';" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; xr-spatial-tracking"></iframe>';
1244 $html .= '</div>';
1245 $html .= '<div id="' . esc_attr( $random_id . '-drag-surface' ) . '" style="position:absolute; top:60px; left:0; right:0; bottom:48px; display:none; pointer-events:none; cursor:grab; z-index:3; background:transparent;"></div>';
1246
1247 // Custom control bar
1248 $html .= '<div id="' . esc_attr( $random_id . '-controls' ) . '" style="display:none; position:absolute; bottom:0; left:0; right:0; height:48px; z-index:5; pointer-events:auto; background:linear-gradient(transparent, rgba(0,0,0,0.7)); align-items:center; padding:0 8px; gap:6px; box-sizing:border-box;">';
1249
1250 // Play/Pause button
1251 $html .= '<button type="button" id="' . esc_attr( $random_id . '-play-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0;" title="Play">';
1252 $html .= '<svg id="' . esc_attr( $random_id . '-play-icon' ) . '" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M8 5v14l11-7z"/></svg>';
1253 $html .= '</button>';
1254
1255 // Time current
1256 $html .= '<span id="' . esc_attr( $random_id . '-time-current' ) . '" style="color:#fff; font-size:12px; font-family:Arial,sans-serif; min-width:36px; text-align:center; user-select:none;">0:00</span>';
1257
1258 // Progress bar
1259 $html .= '<div id="' . esc_attr( $random_id . '-progress-wrap' ) . '" style="flex:1; height:4px; background:rgba(255,255,255,0.3); border-radius:2px; cursor:pointer; position:relative;">';
1260 $html .= '<div id="' . esc_attr( $random_id . '-progress-buffered' ) . '" style="position:absolute; top:0; left:0; height:100%; background:rgba(255,255,255,0.4); border-radius:2px; pointer-events:none;"></div>';
1261 $html .= '<div id="' . esc_attr( $random_id . '-progress-bar' ) . '" style="position:absolute; top:0; left:0; height:100%; background:#ff0000; border-radius:2px; pointer-events:none;"></div>';
1262 $html .= '<div id="' . esc_attr( $random_id . '-progress-thumb' ) . '" style="position:absolute; top:50%; left:0; width:12px; height:12px; background:#ff0000; border-radius:50%; transform:translate(-50%,-50%); pointer-events:none; display:none;"></div>';
1263 $html .= '</div>';
1264
1265 // Time duration
1266 $html .= '<span id="' . esc_attr( $random_id . '-time-duration' ) . '" style="color:#fff; font-size:12px; font-family:Arial,sans-serif; min-width:36px; text-align:center; user-select:none;">0:00</span>';
1267
1268 // Mute button
1269 $html .= '<button id="' . esc_attr( $random_id . '-mute-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0; position:relative;" title="Mute">';
1270 $html .= '<svg id="' . esc_attr( $random_id . '-vol-icon' ) . '" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>';
1271 $html .= '</button>';
1272
1273 // Volume slider (popup on mute hover)
1274 $html .= '<style>#' . esc_attr( $random_id ) . '-vol-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#fff;cursor:pointer;margin-top:-5px;}#' . esc_attr( $random_id ) . '-vol-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#fff;border:none;cursor:pointer;}#' . esc_attr( $random_id ) . '-vol-slider::-webkit-slider-runnable-track{height:4px;border-radius:4px;}#' . esc_attr( $random_id ) . '-vol-slider::-moz-range-track{height:4px;border-radius:4px;background:rgba(255,255,255,0.3);}</style>';
1275 $html .= '<div id="' . esc_attr( $random_id . '-vol-slider-wrap' ) . '" style="display:none; align-items:center; height:40px; padding:0px 4px;">';
1276 $html .= '<input id="' . esc_attr( $random_id . '-vol-slider' ) . '" type="range" min="0" max="100" value="100" style="width:80px; height:4px; cursor:pointer; -webkit-appearance:none; appearance:none; background:linear-gradient(to right,#fff 100%,rgba(255,255,255,0.3) 100%); border-radius:4px; outline:none; padding:0;" />';
1277 $html .= '</div>';
1278
1279 // Fullscreen button
1280 $html .= '<button id="' . esc_attr( $random_id . '-fs-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0;" title="Toggle fullscreen">';
1281 $html .= '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
1282 $html .= '</button>';
1283
1284 $html .= '</div>'; // Close controls
1285
1286 // Add browser compatibility warning message
1287 $html .= '<div id="' . $random_id . '-browser-warning" style="position:absolute; top:0; left:0; width:100%; height:100%; display:none; pointer-events:none; flex-direction:column; justify-content:center; align-items:center; background-color:rgba(0,0,0,0.7); color:white; text-align:center; border-radius:' . $radius . ';">';
1288 $html .= '<p id="' . $random_id . '-warning-text" style="font-size:16px; margin:0 20px 5px;"></p>';
1289 $html .= '<p style="font-size:14px; margin:5px 20px 15px;">For the best experience, use a browser with WebGL support.</p>';
1290 $html .= '<button id="' . $random_id . '-browser-continue" style="padding:10px 15px; background-color:#0085ba; color:#fff; border:none; border-radius:4px; cursor:pointer;">Continue Anyway</button>';
1291 $html .= '</div>';
1292
1293 $html .= '<div id="' . $random_id . '-iphone-fallback" style="position:absolute; top:0; left:0; width:100%; height:100%; display:none; pointer-events:none; flex-direction:column; justify-content:center; align-items:center; background-color:rgba(0,0,0,0.75); color:white; text-align:center; border-radius:' . $radius . ';">';
1294 $html .= '<p id="' . $random_id . '-mobile-fallback-title" style="font-size:16px; margin:0 20px 8px;">YouTube 360 is limited in mobile browser embeds.</p>';
1295 $html .= '<p id="' . $random_id . '-mobile-fallback-description" style="font-size:14px; margin:0 20px 15px;">For proper 360 playback, open this video in the YouTube app.</p>';
1296 $html .= '<a id="' . $random_id . '-iphone-open" href="https://www.youtube.com/watch?v=' . esc_attr( $expdata ) . '" target="_blank" rel="noopener noreferrer" style="color:rgba(255,255,255,0.85); text-decoration:underline; font-size:12px; margin-bottom:8px;">Open in YouTube</a>';
1297 $html .= '<a id="' . $random_id . '-iphone-continue" href="#" style="color:rgba(255,255,255,0.75); text-decoration:underline; font-size:12px;">Show Here Anyway</a>';
1298 $html .= '</div>';
1299
1300 // Main script for handling compatibility and permissions
1301 $html .= '<script>
1302 // Wait for DOM to be fully loaded
1303 document.addEventListener("DOMContentLoaded", function() {
1304 var playerInitialised = false;
1305 var ytPlayer = null;
1306 var dragSurface = document.getElementById("' . $random_id . '-drag-surface");
1307 var isDragging = false;
1308 var dragStartX = 0;
1309 var dragStartY = 0;
1310 var dragStartView = null;
1311
1312 function clamp(value, min, max) {
1313 return Math.min(Math.max(value, min), max);
1314 }
1315
1316 function getSphericalView() {
1317 if (!ytPlayer || typeof ytPlayer.getSphericalProperties !== "function") {
1318 return null;
1319 }
1320
1321 var view = ytPlayer.getSphericalProperties();
1322 return view && Object.keys(view).length ? view : null;
1323 }
1324
1325 function setDragSurfaceEnabled(enabled) {
1326 if (!dragSurface) {
1327 return;
1328 }
1329
1330 dragSurface.style.display = enabled ? "block" : "none";
1331 dragSurface.style.pointerEvents = enabled ? "auto" : "none";
1332 }
1333
1334 function getEventPoint(event) {
1335 if (event.touches && event.touches.length) {
1336 return event.touches[0];
1337 }
1338
1339 if (event.changedTouches && event.changedTouches.length) {
1340 return event.changedTouches[0];
1341 }
1342
1343 return event;
1344 }
1345
1346 function beginDrag(event) {
1347 var point = getEventPoint(event);
1348 var sphericalView = getSphericalView();
1349
1350 if (!sphericalView || !point) {
1351 return;
1352 }
1353
1354 event.preventDefault();
1355 dragStartX = point.clientX;
1356 dragStartY = point.clientY;
1357 dragStartView = sphericalView;
1358 dragSurface.style.cursor = "grabbing";
1359 }
1360
1361 function updateDrag(event) {
1362 var deltaX;
1363 var deltaY;
1364 var nextView;
1365 var point = getEventPoint(event);
1366
1367 if (!point) {
1368 return;
1369 }
1370
1371 if (!isDragging && dragStartView) {
1372 deltaX = point.clientX - dragStartX;
1373 deltaY = point.clientY - dragStartY;
1374 if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 5) {
1375 isDragging = true;
1376 }
1377 }
1378
1379 if (!isDragging || !dragStartView || !ytPlayer || typeof ytPlayer.setSphericalProperties !== "function") {
1380 return;
1381 }
1382
1383 event.preventDefault();
1384 deltaX = point.clientX - dragStartX;
1385 deltaY = point.clientY - dragStartY;
1386
1387 nextView = {
1388 yaw: (dragStartView.yaw || 0) + (deltaX * 0.2),
1389 pitch: clamp((dragStartView.pitch || 0) + (deltaY * 0.2), -85, 85),
1390 roll: dragStartView.roll || 0,
1391 fov: dragStartView.fov || 100
1392 };
1393
1394 ytPlayer.setSphericalProperties(nextView);
1395 }
1396
1397 function endDrag() {
1398 if (!isDragging && dragStartView && ytPlayer) {
1399 var playerState = ytPlayer.getPlayerState();
1400 if (playerState === 1) {
1401 ytPlayer.pauseVideo();
1402 } else {
1403 ytPlayer.playVideo();
1404 }
1405 }
1406 isDragging = false;
1407 dragStartView = null;
1408 if (dragSurface) {
1409 dragSurface.style.cursor = "grab";
1410 }
1411 }
1412
1413 function update360DragAvailability() {
1414 var sphericalView = getSphericalView();
1415 setDragSurfaceEnabled(!!sphericalView);
1416 }
1417
1418 function schedule360AvailabilityChecks() {
1419 update360DragAvailability();
1420 window.setTimeout(update360DragAvailability, 500);
1421 window.setTimeout(update360DragAvailability, 1500);
1422 window.setTimeout(update360DragAvailability, 3000);
1423 }
1424
1425 if (dragSurface) {
1426 dragSurface.addEventListener("mousedown", function(event) {
1427 beginDrag(event);
1428 });
1429
1430 dragSurface.addEventListener("touchstart", function(event) {
1431 beginDrag(event);
1432 }, { passive: false });
1433
1434 dragSurface.addEventListener("dragstart", function(event) {
1435 event.preventDefault();
1436 });
1437
1438 window.addEventListener("mousemove", function(event) {
1439 updateDrag(event);
1440 });
1441
1442 window.addEventListener("touchmove", function(event) {
1443 updateDrag(event);
1444 }, { passive: false });
1445
1446 window.addEventListener("mouseup", function() {
1447 endDrag();
1448 });
1449
1450 window.addEventListener("touchend", function() {
1451 endDrag();
1452 });
1453
1454 window.addEventListener("touchcancel", function() {
1455 endDrag();
1456 });
1457 }
1458
1459 function wpvrLoadYouTubeApi(callback) {
1460 window.wpvrYoutubeApiCallbacks = window.wpvrYoutubeApiCallbacks || [];
1461
1462 if (window.YT && window.YT.Player) {
1463 callback();
1464 return;
1465 }
1466
1467 window.wpvrYoutubeApiCallbacks.push(callback);
1468
1469 if (window.wpvrYoutubeApiLoading) {
1470 return;
1471 }
1472
1473 window.wpvrYoutubeApiLoading = true;
1474
1475 var previousReady = window.onYouTubeIframeAPIReady;
1476 window.onYouTubeIframeAPIReady = function() {
1477 if (typeof previousReady === "function") {
1478 previousReady();
1479 }
1480
1481 var callbacks = window.wpvrYoutubeApiCallbacks || [];
1482 while (callbacks.length) {
1483 var queuedCallback = callbacks.shift();
1484 if (typeof queuedCallback === "function") {
1485 queuedCallback();
1486 }
1487 }
1488 };
1489
1490 var tag = document.createElement("script");
1491 tag.src = "https://www.youtube.com/iframe_api";
1492 var firstScriptTag = document.getElementsByTagName("script")[0];
1493 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
1494 }
1495
1496 function initializeYouTubePlayer() {
1497 if (playerInitialised) {
1498 return;
1499 }
1500
1501 wpvrLoadYouTubeApi(function() {
1502 if (playerInitialised || !(window.YT && window.YT.Player)) {
1503 return;
1504 }
1505
1506 ytPlayer = new YT.Player("' . esc_js( $random_id . '-iframe' ) . '", {
1507 events: {
1508 onReady: function() {
1509 schedule360AvailabilityChecks();
1510 startProgressLoop();
1511 scheduleIPhoneFallbackCheck();
1512 },
1513 onStateChange: function() {
1514 schedule360AvailabilityChecks();
1515 startProgressLoop();
1516 scheduleIPhoneFallbackCheck();
1517 }
1518 }
1519 });
1520 playerInitialised = true;
1521 });
1522 }
1523
1524 // Check compatibility first
1525 var supportInfo = wpvr_check_360_support();
1526 var frameContainer = document.getElementById("' . $random_id . '-frame");
1527 let compatibilityCheck = document.getElementById("' . $random_id . '-compatibility-check");
1528 var iphoneFallback = document.getElementById("' . $random_id . '-iphone-fallback");
1529 var iphoneContinue = document.getElementById("' . $random_id . '-iphone-continue");
1530 var mobileFallbackTitle = document.getElementById("' . $random_id . '-mobile-fallback-title");
1531 var mobileFallbackDescription = document.getElementById("' . $random_id . '-mobile-fallback-description");
1532
1533 function updateMobileFallbackContent() {
1534 if (!mobileFallbackTitle || !mobileFallbackDescription) {
1535 return;
1536 }
1537
1538 if (supportInfo.isIPhone) {
1539 mobileFallbackTitle.textContent = "YouTube 360 is limited in iPhone Safari embeds.";
1540 mobileFallbackDescription.textContent = "For proper 360 playback, open this video in the YouTube app.";
1541 } else if (supportInfo.isAndroid) {
1542 mobileFallbackTitle.textContent = "YouTube 360 is limited in Android browser embeds.";
1543 mobileFallbackDescription.textContent = "For reliable 360 playback, open this video in the YouTube app.";
1544 } else {
1545 mobileFallbackTitle.textContent = "YouTube 360 is limited in mobile browser embeds.";
1546 mobileFallbackDescription.textContent = "For proper 360 playback, open this video in the YouTube app.";
1547 }
1548 }
1549
1550 function hideIPhoneFallback() {
1551 if (!iphoneFallback) {
1552 return;
1553 }
1554
1555 iphoneFallback.style.display = "none";
1556 iphoneFallback.style.pointerEvents = "none";
1557 }
1558
1559 function showIPhoneFallback() {
1560 if (!iphoneFallback) {
1561 return;
1562 }
1563
1564 frameContainer.style.display = "none";
1565 iphoneFallback.style.display = "flex";
1566 iphoneFallback.style.pointerEvents = "auto";
1567 }
1568
1569 function scheduleIPhoneFallbackCheck() {
1570 if (!supportInfo.isIPhone && !supportInfo.isAndroid) {
1571 return;
1572 }
1573
1574 updateMobileFallbackContent();
1575
1576 window.setTimeout(function() {
1577 if (!getSphericalView()) {
1578 showIPhoneFallback();
1579 }
1580 }, 1800);
1581 }
1582
1583 if (iphoneContinue) {
1584 iphoneContinue.addEventListener("click", function(event) {
1585 event.preventDefault();
1586 hideIPhoneFallback();
1587 showVideo();
1588 });
1589 }
1590
1591 if (supportInfo.isMobile && !supportInfo.fullySupported) {
1592 compatibilityCheck.style.display = "flex";
1593 compatibilityCheck.style.pointerEvents = "auto";
1594 }
1595 // Check browser compatibility and handle overlays accordingly.
1596 // The iframe is already visible by default so the YouTube 360° WebGL
1597 // renderer gets the correct dimensions during initialisation.
1598 setTimeout(function() {
1599 document.getElementById("' . $random_id . '-compatibility-check").style.display = "none";
1600 document.getElementById("' . $random_id . '-compatibility-check").style.pointerEvents = "none";
1601 // If browser doesn\'t support 360 videos well, hide the iframe and show warning
1602 if (!supportInfo.supported || supportInfo.browserWarning) {
1603 document.getElementById("' . $random_id . '-frame").style.display = "none";
1604 document.getElementById("' . $random_id . '-warning-text").textContent =
1605 supportInfo.browserWarning || "Your browser may not fully support 360° videos.";
1606 var warningEl = document.getElementById("' . $random_id . '-browser-warning");
1607 warningEl.style.display = "flex";
1608 warningEl.style.pointerEvents = "auto";
1609
1610 // Add continue anyway button handler
1611 document.getElementById("' . $random_id . '-browser-continue").addEventListener("click", function() {
1612 warningEl.style.display = "none";
1613 warningEl.style.pointerEvents = "none";
1614 showVideo();
1615 });
1616 }
1617 // Otherwise (supported desktop browser): iframe is already visible, nothing to do.
1618 else {
1619 initializeYouTubePlayer();
1620 }
1621 }, 500);
1622
1623 function showVideo() {
1624 hideIPhoneFallback();
1625 frameContainer.style.display = "block";
1626 initializeYouTubePlayer();
1627 }
1628
1629 // === Custom control bar logic ===
1630 var outerContainer = document.getElementById("' . $random_id . '-container");
1631 var controlBar = document.getElementById("' . $random_id . '-controls");
1632 var playBtn = document.getElementById("' . $random_id . '-play-btn");
1633 var playIcon = document.getElementById("' . $random_id . '-play-icon");
1634 var timeCurrent = document.getElementById("' . $random_id . '-time-current");
1635 var timeDuration = document.getElementById("' . $random_id . '-time-duration");
1636 var progressWrap = document.getElementById("' . $random_id . '-progress-wrap");
1637 var progressBar = document.getElementById("' . $random_id . '-progress-bar");
1638 var progressBuffered = document.getElementById("' . $random_id . '-progress-buffered");
1639 var progressThumb = document.getElementById("' . $random_id . '-progress-thumb");
1640 var fsBtn = document.getElementById("' . $random_id . '-fs-btn");
1641 var frameEl = document.getElementById("' . $random_id . '-frame");
1642 var muteBtn = document.getElementById("' . $random_id . '-mute-btn");
1643 var volSlider = document.getElementById("' . $random_id . '-vol-slider");
1644 var volSliderWrap = document.getElementById("' . $random_id . '-vol-slider-wrap");
1645 var volIcon = document.getElementById("' . $random_id . '-vol-icon");
1646
1647 var playPath = "M8 5v14l11-7z";
1648 var pausePath = "M6 19h4V5H6v14zm8-14v14h4V5h-4z";
1649 var volIconMuted = "M16.5 12A4.5 4.5 0 0014 8.14v2.07l2.45 2.45c.03-.21.05-.43.05-.66zm2.5 0c0 .93-.21 1.82-.58 2.61l1.47 1.47A8.94 8.94 0 0021 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 003.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z";
1650 var volIconLow = "M7 9v6h4l5 5V4L7 9z";
1651 var volIconMed = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86z";
1652 var volIconHigh = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z";
1653
1654 function formatTime(sec) {
1655 if (!sec || isNaN(sec)) return "0:00";
1656 sec = Math.floor(sec);
1657 var m = Math.floor(sec / 60);
1658 var s = sec % 60;
1659 return m + ":" + (s < 10 ? "0" : "") + s;
1660 }
1661
1662 // Show control bar once player exists
1663 if (controlBar) controlBar.style.display = "flex";
1664
1665 // --- Play / Pause ---
1666 if (playBtn) {
1667 playBtn.addEventListener("click", function() {
1668 if (!ytPlayer) return;
1669 var state = ytPlayer.getPlayerState();
1670 if (state === 1) { ytPlayer.pauseVideo(); }
1671 else { ytPlayer.playVideo(); }
1672 });
1673 }
1674
1675 function updatePlayIcon() {
1676 if (!ytPlayer || !playIcon) return;
1677 var state = ytPlayer.getPlayerState();
1678 playIcon.querySelector("path").setAttribute("d", state === 1 ? pausePath : playPath);
1679 }
1680
1681 // --- Progress bar ---
1682 var progressAnimId = null;
1683 var isSeeking = false;
1684 var progressLoopStarted = false;
1685
1686 function startProgressLoop() {
1687 if (progressLoopStarted) return;
1688 progressLoopStarted = true;
1689 progressAnimId = requestAnimationFrame(updateProgress);
1690 }
1691
1692 function updateProgress() {
1693 if (!ytPlayer || isSeeking) { progressAnimId = requestAnimationFrame(updateProgress); return; }
1694 try {
1695 var current = typeof ytPlayer.getCurrentTime === "function" ? ytPlayer.getCurrentTime() : 0;
1696 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
1697 if (duration > 0) {
1698 var pct = (current / duration) * 100;
1699 progressBar.style.width = pct + "%";
1700 progressThumb.style.left = pct + "%";
1701 timeCurrent.textContent = formatTime(current);
1702 timeDuration.textContent = formatTime(duration);
1703 }
1704 if (typeof ytPlayer.getVideoLoadedFraction === "function") {
1705 progressBuffered.style.width = (ytPlayer.getVideoLoadedFraction() * 100) + "%";
1706 }
1707 updatePlayIcon();
1708 } catch (e) {
1709 // Player API not ready yet, retry on next frame
1710 }
1711 progressAnimId = requestAnimationFrame(updateProgress);
1712 }
1713
1714 // Seek on click / drag
1715 if (progressWrap) {
1716 function seekFromEvent(e) {
1717 var rect = progressWrap.getBoundingClientRect();
1718 var x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
1719 var pct = x / rect.width;
1720 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
1721 if (duration > 0 && ytPlayer) {
1722 ytPlayer.seekTo(pct * duration, true);
1723 progressBar.style.width = (pct * 100) + "%";
1724 progressThumb.style.left = (pct * 100) + "%";
1725 timeCurrent.textContent = formatTime(pct * duration);
1726 }
1727 }
1728
1729 progressWrap.addEventListener("mousedown", function(e) {
1730 isSeeking = true;
1731 seekFromEvent(e);
1732 function onMove(e) { seekFromEvent(e); }
1733 function onUp() {
1734 isSeeking = false;
1735 document.removeEventListener("mousemove", onMove);
1736 document.removeEventListener("mouseup", onUp);
1737 }
1738 document.addEventListener("mousemove", onMove);
1739 document.addEventListener("mouseup", onUp);
1740 });
1741
1742 progressWrap.addEventListener("mouseenter", function() {
1743 progressThumb.style.display = "block";
1744 });
1745 progressWrap.addEventListener("mouseleave", function() {
1746 if (!isSeeking) progressThumb.style.display = "none";
1747 });
1748 }
1749
1750 // --- Volume ---
1751 var volHideTimer = null;
1752 var localVol = 100;
1753 var localMuted = false;
1754
1755 function updateVolIconFromLocal() {
1756 if (!volIcon) return;
1757 var path;
1758 if (localMuted || localVol === 0) { path = volIconMuted; }
1759 else if (localVol < 40) { path = volIconLow; }
1760 else if (localVol < 75) { path = volIconMed; }
1761 else { path = volIconHigh; }
1762 volIcon.querySelector("path").setAttribute("d", path);
1763 }
1764
1765 // Called from schedule360AvailabilityChecks — only syncs icon, never touches slider
1766 function updateVolIcon() {
1767 updateVolIconFromLocal();
1768 }
1769
1770 function showVolSlider() {
1771 if (volHideTimer) { clearTimeout(volHideTimer); volHideTimer = null; }
1772 if (volSliderWrap) volSliderWrap.style.display = "flex";
1773 }
1774 function hideVolSlider() {
1775 volHideTimer = setTimeout(function() { if (volSliderWrap) volSliderWrap.style.display = "none"; }, 400);
1776 }
1777
1778 if (muteBtn) {
1779 muteBtn.addEventListener("click", function() {
1780 if (!ytPlayer) return;
1781 if (localMuted) {
1782 ytPlayer.unMute();
1783 localMuted = false;
1784 if (localVol === 0) { localVol = 100; ytPlayer.setVolume(100); }
1785 if (volSlider) volSlider.value = localVol;
1786 } else {
1787 ytPlayer.mute();
1788 localMuted = true;
1789 if (volSlider) volSlider.value = 0;
1790 }
1791 updateVolIconFromLocal();
1792 updateVolSliderFill();
1793 });
1794 muteBtn.addEventListener("mouseenter", showVolSlider);
1795 muteBtn.addEventListener("mouseleave", hideVolSlider);
1796 }
1797 if (volSliderWrap) {
1798 volSliderWrap.addEventListener("mouseenter", showVolSlider);
1799 volSliderWrap.addEventListener("mouseleave", hideVolSlider);
1800 }
1801 function updateVolSliderFill() {
1802 if (!volSlider) return;
1803 var v = parseInt(volSlider.value, 10);
1804 volSlider.style.background = "linear-gradient(to right,#fff " + v + "%,rgba(255,255,255,0.3) " + v + "%)";
1805 }
1806 if (volSlider) {
1807 volSlider.addEventListener("input", function() {
1808 if (!ytPlayer) return;
1809 var val = parseInt(volSlider.value, 10);
1810 localVol = val;
1811 ytPlayer.setVolume(val);
1812 if (val === 0) {
1813 ytPlayer.mute();
1814 localMuted = true;
1815 } else if (localMuted) {
1816 ytPlayer.unMute();
1817 localMuted = false;
1818 }
1819 updateVolIconFromLocal();
1820 updateVolSliderFill();
1821 });
1822 updateVolSliderFill();
1823 }
1824
1825 // --- Fullscreen ---
1826 if (outerContainer) {
1827 var savedFrameWidth = frameEl ? frameEl.style.width : "";
1828 var savedFrameHeight = frameEl ? frameEl.style.height : "";
1829 var savedFrameMaxWidth = frameEl ? frameEl.style.maxWidth : "";
1830 var savedFrameBorderRadius = frameEl ? frameEl.style.borderRadius : "";
1831 var savedContainerBackground = outerContainer.style.background;
1832 if(fsBtn){
1833 fsBtn.addEventListener("click", function() {
1834 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1835 if (fsEl) {
1836 (document.exitFullscreen || document.webkitExitFullscreen).call(document);
1837 } else {
1838 (outerContainer.requestFullscreen || outerContainer.webkitRequestFullscreen).call(outerContainer);
1839 }
1840 });
1841 }
1842 function onFsChange() {
1843 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1844 var isFs = (fsEl === outerContainer);
1845 frameEl.style.width = isFs ? "100%" : savedFrameWidth;
1846 frameEl.style.height = isFs ? "100%" : savedFrameHeight;
1847 frameEl.style.maxWidth = isFs ? "100%" : savedFrameMaxWidth;
1848 frameEl.style.borderRadius = isFs ? "0" : savedFrameBorderRadius;
1849 outerContainer.style.background = isFs ? "#000" : savedContainerBackground;
1850 fsBtn.querySelector("svg path").setAttribute("d", isFs
1851 ? "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
1852 : "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
1853 );
1854 schedule360AvailabilityChecks();
1855 }
1856 document.addEventListener("fullscreenchange", onFsChange);
1857 document.addEventListener("webkitfullscreenchange", onFsChange);
1858 }
1859
1860 // Sync controls after player ready
1861 var origSchedule = schedule360AvailabilityChecks;
1862 schedule360AvailabilityChecks = function() {
1863 origSchedule();
1864 updateVolIcon();
1865 updatePlayIcon();
1866 };
1867 });
1868 </script>';
1869 $html .= '</div>'; // Close container
1870 $html .= '</div>'; // Close outer container
1871 return $html;
1872 }
1873 /**
1874 * Prepare shortcode data for Vimeo url
1875 *
1876 * @param array $postdata
1877 * @param mixed $width
1878 * @param mixed $height
1879 *
1880 * @return string
1881 * @since 8.0.0
1882 */
1883 public function prepare_vimeo_video_shortcode_data($postdata, $width, $height, $autoplay, $loop, $radius)
1884 {
1885 $autoplay_param = ($autoplay == 'on') ? 'autoplay=1&muted=1' : '';
1886 $loop_param = ($loop == 'on') ? 'loop=1' : '';
1887
1888 $vidurl_parts = explode('/', rtrim( $postdata['vidurl'], '/' ) );
1889 $vid_id = end($vidurl_parts);
1890
1891 $query_parts = array_filter( [ $autoplay_param, $loop_param ] );
1892 $foundid = !empty($query_parts) ? $vid_id . '?' . implode('&', $query_parts) : $vid_id;
1893
1894 $iframe_id = 'wpvr-vimeo-' . wp_rand( 10000, 99999 );
1895 $do_autoplay = ( $autoplay == 'on' ) ? 'true' : 'false';
1896 $do_loop = ( $loop == 'on' ) ? 'true' : 'false';
1897
1898 $html = '';
1899 $html .= '<div style="text-align: center; max-width:100%; width:' . $width . '; height:' . $height . '; margin: 0 auto;">';
1900 $html .= '<iframe id="' . esc_attr( $iframe_id ) . '" src="https://player.vimeo.com/video/' . esc_attr( $foundid ) . '" width="' . trim($width, 'px') . '" height="' . trim($height, 'px') . '" style="border-radius: ' . $radius . ';" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>';
1901 $html .= '</div>';
1902 $html .= '<script>
1903 (function() {
1904 var IFRAME_ID = ' . wp_json_encode( $iframe_id ) . ';
1905 var DO_AUTOPLAY = ' . $do_autoplay . ';
1906 var DO_LOOP = ' . $do_loop . ';
1907
1908 function wpvrLoadVimeoSdk( callback ) {
1909 if ( window.Vimeo && window.Vimeo.Player ) { callback(); return; }
1910 window.wpvrVimeoSdkCallbacks = window.wpvrVimeoSdkCallbacks || [];
1911 window.wpvrVimeoSdkCallbacks.push( callback );
1912 if ( window.wpvrVimeoSdkLoading ) { return; }
1913 window.wpvrVimeoSdkLoading = true;
1914 var tag = document.createElement( "script" );
1915 tag.src = "https://player.vimeo.com/api/player.js";
1916 tag.onload = function() {
1917 var cbs = window.wpvrVimeoSdkCallbacks || [];
1918 while ( cbs.length ) { ( cbs.shift() )(); }
1919 };
1920 document.head.appendChild( tag );
1921 }
1922
1923 function initVimeoPlayer() {
1924 var iframeEl = document.getElementById( IFRAME_ID );
1925 if ( !iframeEl ) { return; }
1926 var player = new Vimeo.Player( iframeEl );
1927
1928 player.ready().then( function() {
1929 if ( DO_LOOP ) {
1930 player.setLoop( true ).catch( function() {} );
1931 }
1932 if ( DO_AUTOPLAY ) {
1933 player.setVolume( 0 ).then( function() {
1934 player.play().catch( function() {} );
1935 } ).catch( function() {
1936 player.play().catch( function() {} );
1937 } );
1938 }
1939 } );
1940 }
1941
1942 if ( document.readyState === "loading" ) {
1943 document.addEventListener( "DOMContentLoaded", function() { wpvrLoadVimeoSdk( initVimeoPlayer ); } );
1944 } else {
1945 wpvrLoadVimeoSdk( initVimeoPlayer );
1946 }
1947 }());
1948 </script>';
1949 return $html;
1950 }
1951
1952
1953 /**
1954 * Prepare shortcode data from regular postdata
1955 *
1956 * @param integer $id
1957 * @param array $postdata
1958 * @param string $width
1959 * @param string $height
1960 *
1961 * @return string
1962 * @since 8.0.0
1963 */
1964 public function prepare_regular_video_shortcode_data($id, $postdata, $width, $height, $radius)
1965 {
1966 $html = '';
1967 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="max-width:100%; width: ' . $width . '; height: ' . $height . '; border-radius: ' . $radius . '; margin: 0 auto;">';
1968 $html .= '<div style="width:100%; height:100%; ">' . $postdata['panoviddata'] . '</div>';
1969
1970 $html .= '
1971 <style>
1972 .video-js {
1973 border-radius:' . $radius . ';
1974 }
1975 .video-js canvas{
1976 border-radius:' . $radius . ';
1977 }
1978 #pano' . $id . ' .vjs-poster {
1979 border-radius: ' . $radius . ';
1980 }
1981 </style>
1982
1983 ';
1984
1985 $html .= '</div>';
1986
1987 //video js vr setup //
1988 $html .= '<script>';
1989 $html .= '
1990 (function (window, videojs) {
1991 var player = window.player = videojs("' . $postdata['vidid'] . '");
1992 player.mediainfo = player.mediainfo || {};
1993 player.mediainfo.projection = "equirectangular";
1994
1995 // AUTO is the default and looks at mediainfo
1996 var vr = window.vr = player.vr({ projection: "AUTO", debug: true, forceCardboard: false, antialias: false });
1997 }(window, window.videojs));
1998
1999 ';
2000 $html .= '</script>';
2001 //video js vr end //
2002 return $html;
2003 }
2004
2005
2006 /**
2007 * Prepare scene data for shortcode
2008 *
2009 * @param array $panodata
2010 *
2011 * @return array
2012 * @since 8.0.0
2013 */
2014 public function prepare_shortcode_scene_data($panodata)
2015 {
2016 $scene_data = array();
2017
2018 if (!empty($panodata["scene-list"])) {
2019 $scene_data = $this->prepare_shortcode_scene_list($panodata);
2020 }
2021
2022 return $scene_data;
2023 }
2024
2025
2026 /**
2027 * Prepare scene lists data for shortcode
2028 *
2029 * @param array $panodata
2030 *
2031 * @return array
2032 * @since 8.0.0
2033 */
2034 private function prepare_shortcode_scene_list($panodata)
2035 {
2036 $scene_data = array();
2037 foreach ($panodata["scene-list"] as $panoscenes) {
2038 $hotspot_datas = array();
2039 if (isset($panoscenes["hotspot-list"])) {
2040 $hotspot_datas = $panoscenes["hotspot-list"];
2041 }
2042 $hotspots = $this->prepare_shortcode_hotspots_list($hotspot_datas);
2043 $device_scene = $this->get_shortcode_device_scene($panoscenes['scene-attachment-url']);
2044 $scene_info = $this->get_shortcode_scene_info($panoscenes, $hotspots, $device_scene);
2045 $scene_data[$panoscenes["scene-id"]] = $scene_info;
2046 }
2047 return $scene_data;
2048 }
2049
2050
2051 /**
2052 * Prepare hotspots list for shortcode
2053 *
2054 * @param array $hotspot_datas
2055 *
2056 * @return array
2057 * @since 8.0.0
2058 */
2059 private function prepare_shortcode_hotspots_list($hotspot_datas)
2060 {
2061 $hotspots = array();
2062 foreach ($hotspot_datas as $hotspot_data) {
2063 $hotspot_type = $hotspot_data["hotspot-type"] !== 'scene' ? 'info' : $hotspot_data["hotspot-type"];
2064 $hotspot_content = $hotspot_data["hotspot-content"];
2065 $hotspot_info = $this->get_shortcode_hotspot_info($hotspot_data, $hotspot_type, $hotspot_content);
2066 array_push($hotspots, $hotspot_info);
2067 }
2068 return $hotspots;
2069 }
2070
2071
2072 /**
2073 * Return hotspot info array to hotspot list
2074 *
2075 * @param array $hotspot_data
2076 * @param string $hotspot_type
2077 * @param string $hotspot_content
2078 *
2079 * @return array
2080 * @since 8.0.0
2081 */
2082 private function get_shortcode_hotspot_info($hotspot_data, $hotspot_type, $hotspot_content)
2083 {
2084 $hotspot_info = array(
2085 "text" => $hotspot_data["hotspot-title"],
2086 "pitch" => $hotspot_data["hotspot-pitch"],
2087 "yaw" => $hotspot_data["hotspot-yaw"],
2088 "type" => $hotspot_type,
2089 "cssClass" => $hotspot_data["hotspot-customclass"],
2090 "URL" => $hotspot_data["hotspot-url"],
2091 "wpvr_url_open" => isset($hotspot_data["wpvr_url_open"][0]) ? $hotspot_data["wpvr_url_open"][0] : 'off',
2092 "clickHandlerArgs" => $hotspot_content,
2093 "createTooltipArgs" => $hotspot_data["hotspot-hover"],
2094 "sceneId" => $hotspot_data["hotspot-scene"],
2095 'hotspot_type' => $hotspot_data['hotspot-type']
2096 );
2097
2098 $hotspot_info['URL'] = ($hotspot_data['hotspot-type'] === 'fluent_form' || $hotspot_data['hotspot-type'] === 'wc_product') ? '' : $hotspot_info['URL'];
2099
2100 if ($hotspot_data["hotspot-customclass"] == 'none' || $hotspot_data["hotspot-customclass"] == '') {
2101 unset($hotspot_info["cssClass"]);
2102 }
2103 if (empty($hotspot_data["hotspot-scene"])) {
2104 unset($hotspot_info['targetPitch']);
2105 unset($hotspot_info['targetYaw']);
2106 }
2107 return $hotspot_info;
2108 }
2109
2110
2111 /**
2112 * Return scene info array for scene list
2113 *
2114 * @param array $panoscenes
2115 * @param array $hotspots
2116 * @param string $device_scene
2117 *
2118 * @return array
2119 * @since 8.0.0
2120 */
2121 private function get_shortcode_scene_info($panoscenes, $hotspots, $device_scene)
2122 {
2123 $scene_info = array();
2124 if ($panoscenes["scene-type"] == 'cubemap') {
2125 $pano_attachment = array(
2126 $panoscenes["scene-attachment-url-face0"],
2127 $panoscenes["scene-attachment-url-face1"],
2128 $panoscenes["scene-attachment-url-face2"],
2129 $panoscenes["scene-attachment-url-face3"],
2130 $panoscenes["scene-attachment-url-face4"],
2131 $panoscenes["scene-attachment-url-face5"]
2132 );
2133 $scene_info = array("type" => $panoscenes["scene-type"], "cubeMap" => $pano_attachment, "hotSpots" => $hotspots);
2134 } else {
2135 $scene_info = array("type" => $panoscenes["scene-type"], "panorama" => $device_scene, "hotSpots" => $hotspots);
2136 }
2137 return $scene_info;
2138 }
2139
2140
2141 /**
2142 * Return device scene for shortcode
2143 *
2144 * @param string $device_scene
2145 *
2146 * @return string
2147 * @since 8.0.0
2148 */
2149 private function get_shortcode_device_scene($device_scene)
2150 {
2151 $mobile_media_resize = get_option('mobile_media_resize');
2152 $file_accessible = ini_get('allow_url_fopen');
2153
2154 if ($mobile_media_resize == "true" && $device_scene) {
2155 if ($file_accessible == "1") {
2156 $image_info = getimagesize($device_scene);
2157 if ($image_info[0] > 4096) {
2158 $src_to_id_for_mobile = '';
2159 $src_to_id_for_desktop = '';
2160 if (wpvr_isMobileDevice()) {
2161 $src_to_id_for_mobile = attachment_url_to_postid($device_scene);
2162 if ($src_to_id_for_mobile) {
2163 $mobile_scene = wp_get_attachment_image_src($src_to_id_for_mobile, 'wpvr_mobile');
2164 if ($mobile_scene[3]) {
2165 $device_scene = $mobile_scene[0];
2166 }
2167 }
2168 } else {
2169 $src_to_id_for_desktop = attachment_url_to_postid($device_scene);
2170 if ($src_to_id_for_desktop) {
2171 $desktop_scene = wp_get_attachment_image_src($src_to_id_for_mobile, 'full');
2172 if ($desktop_scene[0]) {
2173 $device_scene = $desktop_scene[0];
2174 }
2175 }
2176 }
2177 }
2178 }
2179 }
2180 return $device_scene;
2181 }
2182
2183
2184 /**
2185 * Prepare json response for shortcode
2186 *
2187 * @param string $autorotation
2188 * @param array $pano_response
2189 * @param string $panoid
2190 * @param integer $autorotationinactivedelay
2191 * @param integer $autorotationstopdelay
2192 *
2193 * @return array
2194 * @since 8.0.0
2195 */
2196 public function prepare_shortcode_response($autorotation, $pano_response, $panoid, $autorotationinactivedelay, $autorotationstopdelay)
2197 {
2198 $pano_id_array = array("panoid" => $panoid);
2199 if (empty($autorotation)) {
2200 unset($pano_response['autoRotate']);
2201 unset($pano_response['autoRotateInactivityDelay']);
2202 unset($pano_response['autoRotateStopDelay']);
2203 }
2204 if (empty($autorotationinactivedelay)) {
2205 unset($pano_response['autoRotateInactivityDelay']);
2206 }
2207 if (empty($autorotationstopdelay)) {
2208 unset($pano_response['autoRotateStopDelay']);
2209 }
2210 $response = array();
2211 $response = array($pano_id_array, $pano_response);
2212 if (!empty($response)) {
2213 $response = json_encode($response);
2214 }
2215 return $response;
2216 }
2217
2218
2219 /**
2220 * Prepare HTML and script contents for scene shortcode
2221 *
2222 * @param mixed $width
2223 * @param mixed $panoid
2224 * @param mixed $hotspoticoncolor
2225 * @param mixed $foreground_color
2226 * @param mixed $hotspotblink
2227 * @param mixed $pulse_color
2228 * @param mixed $radius
2229 * @param mixed $id
2230 * @param mixed $height
2231 * @param mixed $scene_data
2232 * @param mixed $response
2233 * @param mixed $autoload
2234 * @param mixed $default_scene
2235 * @param mixed $postdata
2236 *
2237 * @return string
2238 * @since 8.0.0
2239 */
2240 public function prepare_scene_shortcode_html_content($width, $panoid, $hotspoticoncolor, $foreground_color, $hotspotblink, $pulse_color, $radius, $id, $height, $scene_data, $response, $autoload, $default_scene, $postdata, $mobile_height, $gyro)
2241 {
2242 $html = '';
2243 $html .= '<style>';
2244
2245 if ($width == 'embed') {
2246 $html .= 'body{overflow: hidden;}';
2247 }
2248 // Return ID and Classes for CSS styling //
2249 $html = $this->get_shortcode_content_classes($html, $panoid, $hotspoticoncolor, $foreground_color);
2250 // Return webkit keyframes CSS styling //
2251 if ($hotspotblink == 'on') {
2252 $html = $this->get_shortcode_webkit_keyframes($html, $panoid, $pulse_color);
2253 }
2254 $status = get_option('wpvr_edd_license_status');
2255 if ($status !== false && $status == 'valid') {
2256 if (!$gyro) {
2257 $html .= '#' . $panoid . ' div.pnlm-orientation-button {
2258 display: none;
2259 }';
2260 }
2261 } else {
2262 $html .= '#' . $panoid . ' div.pnlm-orientation-button {
2263 display: none;
2264 }';
2265 }
2266 $html .= '</style>';
2267
2268 // Render pano wrapper contents //
2269 $html = $this->get_shortcode_pano_wrap_content($width, $radius, $html, $id, $height, $mobile_height);
2270 // Render hotspots contents //
2271 $html = $this->render_shortcode_hotspot_content($html, $id, $scene_data);
2272 // Render custom iframe content //
2273 $html = $this->render_shortcode_custom_iframe_wrapper($html, $id);
2274
2275 $html .= '</div>';
2276
2277 //script started
2278 $html = $this->get_shortcode_script_content($html, $response, $id, $autoload, $default_scene, $postdata);
2279 //script end
2280 return $html;
2281 }
2282
2283
2284 /**
2285 * Return ID and Classes for CSS styling
2286 *
2287 * @param mixed $html
2288 * @param mixed $panoid
2289 * @param mixed $hotspoticoncolor
2290 * @param mixed $foreground_color
2291 *
2292 * @return string
2293 * @since 8.0.0
2294 */
2295 private function get_shortcode_content_classes($html, $panoid, $hotspoticoncolor, $foreground_color)
2296 {
2297 $html .= '#' . $panoid . ' div.pnlm-hotspot-base.fas,
2298 #' . $panoid . ' div.pnlm-hotspot-base.fab,
2299 #' . $panoid . ' div.pnlm-hotspot-base.fa,
2300 #' . $panoid . ' div.pnlm-hotspot-base.fa-solid,
2301 #' . $panoid . ' div.pnlm-hotspot-base.far {
2302 display: block !important;
2303 background-color: ' . $hotspoticoncolor . ';
2304 color: ' . $foreground_color . ';
2305 border-radius: 100%;
2306 width: 30px;
2307 height: 30px;
2308 animation: icon-pulse' . $panoid . ' 1.5s infinite cubic-bezier(.25, 0, 0, 1);
2309 }';
2310 return $html;
2311 }
2312
2313
2314 /**
2315 * Resturn webkit keyframes CSS styling
2316 *
2317 * @param mixed $html
2318 * @param mixed $panoid
2319 * @param mixed $pulse_color
2320 *
2321 * @return string
2322 * @since 8.0.0
2323 */
2324 private function get_shortcode_webkit_keyframes($html, $panoid, $pulse_color)
2325 {
2326 $html .= '@-webkit-keyframes icon-pulse' . $panoid . ' {
2327 0% {
2328 box-shadow: 0 0 0 0px rgba(' . $pulse_color[0] . ', 1);
2329 }
2330 100% {
2331 box-shadow: 0 0 0 10px rgba(' . $pulse_color[0] . ', 0);
2332 }
2333 }
2334 @keyframes icon-pulse' . $panoid . ' {
2335 0% {
2336 box-shadow: 0 0 0 0px rgba(' . $pulse_color[0] . ', 1);
2337 }
2338 100% {
2339 box-shadow: 0 0 0 10px rgba(' . $pulse_color[0] . ', 0);
2340 }
2341 }';
2342 return $html;
2343 }
2344
2345
2346 /**
2347 * Render pano wrapper content depending on Tour Width, Height and radius
2348 *
2349 * @param mixed $width
2350 * @param mixed $radius
2351 * @param mixed $html
2352 * @param mixed $id
2353 * @param mixed $height
2354 *
2355 * @return string
2356 * @since 8.0.0
2357 */
2358 private function get_shortcode_pano_wrap_content($width, $radius, $html, $id, $height, $mobile_height)
2359 {
2360 if ($width == 'fullwidth') {
2361
2362 if (wpvr_isMobileDevice()) {
2363 if ($radius) {
2364 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="text-align:center; border-radius:' . $radius . '; direction:ltr;">';
2365 } else {
2366 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="text-align:center;">';
2367 }
2368 } else {
2369 if ($radius) {
2370 $html .= '<div id="pano' . $id . '" class="pano-wrap vrfullwidth" style=" text-align:center; height: ' . $height . '; border-radius:' . $radius . '; direction:ltr;" >';
2371 } else {
2372 $html .= '<div id="pano' . $id . '" class="pano-wrap vrfullwidth" style=" text-align:center; height: ' . $height . '; direction:ltr;" >';
2373 }
2374 }
2375 } elseif ($width == 'embed') {
2376 $html .= '<div id="pano' . $id . '" class="pano-wrap vrembed" style=" text-align:center; direction:ltr;" >';
2377 } else {
2378 if (wpvr_isMobileDevice()) {
2379 if ($radius) {
2380 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $mobile_height . '!important; margin: 0 auto; border-radius:' . $radius . '; direction:ltr;">';
2381 } else {
2382 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $mobile_height . '!important; margin: 0 auto; direction:ltr;">';
2383 }
2384 } else {
2385 if ($radius) {
2386 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $height . '; margin: 0 auto; border-radius:' . $radius . '; direction:ltr;">';
2387 } else {
2388 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $height . '; margin: 0 auto; direction:ltr;">';
2389 }
2390 }
2391 }
2392 return $html;
2393 }
2394
2395
2396 /**
2397 * Render hotspot contents inside pano wrapper
2398 *
2399 * @param string $html
2400 * @param mixed $id
2401 * @param array $scene_data
2402 *
2403 * @return string
2404 * @since 8.0.0
2405 */
2406 private function render_shortcode_hotspot_content($html, $id, $scene_data)
2407 {
2408 $html .= '<div class="wpvr-hotspot-tweak-contents-wrapper" style="display: none">';
2409 $html .= '<i class="fa fa-times cross" data-id="' . $id . '"></i>';
2410 $html .= '<div class="wpvr-hotspot-tweak-contents-flex">';
2411 $html .= '<div class="wpvr-hotspot-tweak-contents">';
2412 ob_start();
2413 do_action('wpvr_hotspot_tweak_contents', $scene_data);
2414 $hotspot_content = ob_get_clean();
2415 $html .= $hotspot_content;
2416 $html .= '</div>';
2417 $html .= '</div>';
2418 $html .= '</div>';
2419 return $html;
2420 }
2421
2422
2423 /**
2424 * Render custom iframe wrapper inside pano wrapper
2425 *
2426 * @param string $html
2427 * @param mixed $id
2428 *
2429 * @return string
2430 * @since 8.0.0
2431 */
2432 private function render_shortcode_custom_iframe_wrapper($html, $id)
2433 {
2434 $html .= '<div class="custom-ifram-wrapper" style="display: none;">';
2435 $html .= '<i class="fa fa-times cross" data-id="' . $id . '"></i>';
2436
2437 $html .= '<div class="custom-ifram-flex">';
2438 $html .= '<div class="custom-ifram">';
2439 $html .= '</div>';
2440 $html .= '</div>';
2441 $html .= '</div>';
2442 return $html;
2443 }
2444
2445
2446 /**
2447 * Return scripts contents for shortcode
2448 *
2449 * @param string $html
2450 * @param mixed $response
2451 * @param mixed $id
2452 * @param mixed $autoload
2453 * @param mixed $default_scene
2454 * @param mixed $postdata
2455 *
2456 * @return string
2457 * @since 8.0.0
2458 */
2459 private function get_shortcode_script_content($html, $response, $id, $autoload, $default_scene, $postdata)
2460 {
2461 $html .= '<script>';
2462 $html .= 'jQuery(document).ready(function() {';
2463 $html .= 'var response = ' . $response . ';';
2464 $html .= 'var scenes = response[1];';
2465 // Hotspot scripts content //
2466 $html = $this->get_shortcode_hotspot_script_content($html);
2467
2468 $html .= 'var panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);';
2469
2470 // Panaromic show scripts content //
2471 $html = $this->render_shortcode_panoshow_script_content($html, $id);
2472 // Auto rotation scripts content //
2473 $html = $this->render_shortcode_autoRotate_content($html, $id);
2474
2475 $html .= 'var touchtime = 0;';
2476
2477 // Return autoload scripts content //
2478 if (!$autoload) {
2479 $html = $this->render_shortcode_autoload_content($html, $id);
2480 }
2481
2482 // JQuery scripts for on-click content for elementor tab //
2483 $previeword = "Click to Load Panorama";
2484 if (isset($postdata['previewtext']) && $postdata['previewtext'] != '') {
2485 $previeword = $postdata['previewtext'];
2486 }
2487 $html = $this->render_elementor_tab_title_click_content($html, $id, $default_scene, $previeword);
2488 // JQuery scripts for on-click content for vr tour tab //
2489 $html = $this->render_vr_tour_tab_click_content($html, $id, $default_scene);
2490
2491 if (isset($previeword ) && $previeword != '') {
2492 $html .= 'jQuery("#pano' . $id . '").children(".pnlm-ui").find(".pnlm-load-button p").text("' . $previeword . '")';
2493 }
2494
2495 $html .= '});';
2496 $html .= '</script>';
2497 return $html;
2498 }
2499
2500
2501 /**
2502 * Return scripts content for hotspot data on shortcode
2503 *
2504 * @param mixed $html
2505 *
2506 * @return string
2507 * @since 8.0.0
2508 */
2509 private function get_shortcode_hotspot_script_content($html)
2510 {
2511 $html .= 'if(scenes) {';
2512 $html .= 'var scenedata = scenes.scenes;';
2513 $html .= 'for(var i in scenedata) {';
2514 $html .= 'var scenehotspot = scenedata[i].hotSpots;';
2515 $html .= 'for(var i = 0; i < scenehotspot.length; i++) {';
2516 $html .= 'if(scenehotspot[i]["clickHandlerArgs"] != "") {';
2517 $html .= 'scenehotspot[i]["clickHandlerFunc"] = wpvrhotspot;';
2518 $html .= '}';
2519 if (wpvr_isMobileDevice() && get_option('dis_on_hover') == "true") {
2520 } else {
2521 $html .= 'if(scenehotspot[i]["createTooltipArgs"] != "") {';
2522 $html .= 'scenehotspot[i]["createTooltipFunc"] = wpvrtooltip;';
2523 $html .= '}';
2524 }
2525 $html .= '}';
2526 $html .= '}';
2527 $html .= '}';
2528 return $html;
2529 }
2530
2531
2532 /**
2533 * Render scripts content for panaromic show on shortcode
2534 *
2535 * @param mixed $html
2536 * @param mixed $id
2537 *
2538 * @return string
2539 * @since 8.0.0
2540 */
2541 private function render_shortcode_panoshow_script_content($html, $id)
2542 {
2543 $html .= 'panoshow' . $id . '.on("load", function (){
2544 setTimeout(() => {
2545 window.dispatchEvent(new Event("resize"));
2546 }, 200);
2547 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2548 jQuery("#controls' . $id . '").css("bottom", "55px");
2549 }
2550 else {
2551 jQuery("#controls' . $id . '").css("bottom", "5px");
2552 }
2553 });';
2554 $html .= 'panoshow' . $id . '.on("render", function (){
2555 window.dispatchEvent(new Event("resize"));
2556 });';
2557 return $html;
2558 }
2559
2560
2561 /**
2562 * Return auto rotation scripts content on shortcode
2563 *
2564 * @param mixed $html
2565 * @param mixed $id
2566 *
2567 * @return string
2568 * @since 8.0.0
2569 */
2570 private function render_shortcode_autoRotate_content($html, $id)
2571 {
2572 $html .= 'if (scenes.autoRotate) {
2573 panoshow' . $id . '.on("load", function (){
2574 setTimeout(function(){ panoshow' . $id . '.startAutoRotate(scenes.autoRotate, 0); }, 3000);
2575 });
2576 panoshow' . $id . '.on("scenechange", function (){
2577 setTimeout(function(){ panoshow' . $id . '.startAutoRotate(scenes.autoRotate, 0); }, 3000);
2578 });
2579 }';
2580 return $html;
2581 }
2582
2583
2584 /**
2585 * Return autoload scripts content on shortcode
2586 *
2587 * @param mixed $html
2588 * @param mixed $id
2589 *
2590 * @return string
2591 * @since 8.0.0
2592 */
2593 private function render_shortcode_autoload_content($html, $id)
2594 {
2595 $html .= 'jQuery(document).ready(function(){
2596 jQuery("#controls' . $id . '").hide();
2597 jQuery("#zoom-in-out-controls' . $id . '").hide();
2598 jQuery("#adcontrol' . $id . '").hide();
2599 jQuery("#pano' . $id . '").find(".pnlm-panorama-info").hide();
2600 });';
2601
2602 $html .= 'panoshow' . $id . '.on("load", function (){
2603 jQuery("#controls' . $id . '").show();
2604 jQuery("#zoom-in-out-controls' . $id . '").show();
2605 jQuery("#adcontrol' . $id . '").show();
2606 jQuery("#pano' . $id . '").find(".pnlm-panorama-info").show();
2607 });';
2608 return $html;
2609 }
2610
2611
2612 /**
2613 * JQuery scripts for on-click content for elementor tab
2614 *
2615 * @param mixed $html
2616 * @param mixed $id
2617 * @param mixed $default_scene
2618 *
2619 * @return string
2620 * @since 8.0.0
2621 */
2622 private function render_elementor_tab_title_click_content($html, $id, $default_scene, $previeword)
2623 {
2624 $html .= 'jQuery(".elementor-tab-title").click(function(){
2625 var element_id;
2626 var pano_id;
2627 var element_id = this.id;
2628 element_id = element_id.split("-");
2629 element_id = element_id[3];
2630 jQuery("#elementor-tab-content-"+element_id).children("div").addClass("awwww");
2631 var pano_id = jQuery(".awwww").attr("id");
2632 jQuery("#elementor-tab-content-"+element_id).children("div").removeClass("awwww");
2633 if (pano_id != undefined) {
2634 pano_id = pano_id.split("o");
2635 pano_id = pano_id[1];
2636 if (pano_id == "' . $id . '") {
2637 jQuery("#pano' . $id . '").children(".pnlm-render-container").remove();
2638 jQuery("#pano' . $id . '").children(".pnlm-ui").remove();
2639 panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);
2640 jQuery("#pano' . $id . '").children(".pnlm-ui").find(".pnlm-load-button p").text("' . $previeword . '")
2641 setTimeout(function() {
2642 // panoshow' . $id . '.loadScene("' . $default_scene . '");
2643 window.dispatchEvent(new Event("resize"));
2644 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2645 jQuery("#controls' . $id . '").css("bottom", "55px");
2646 }
2647 else {
2648 jQuery("#controls' . $id . '").css("bottom", "5px");
2649 }
2650 }, 200);
2651 }
2652 }
2653 });';
2654 return $html;
2655 }
2656
2657
2658 /**
2659 * JQuery scripts for on-click content for vr tour tab
2660 *
2661 * @param mixed $html
2662 * @param mixed $id
2663 * @param mixed $default_scene
2664 *
2665 * @return string
2666 * @since 8.0.0
2667 */
2668 private function render_vr_tour_tab_click_content($html, $id, $default_scene)
2669 {
2670 $html .= 'jQuery(".geodir-tab-head dd, #vr-tour-tab").click(function(){
2671 jQuery("#pano' . $id . '").children(".pnlm-render-container").remove();
2672 jQuery("#pano' . $id . '").children(".pnlm-ui").remove();
2673 panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);
2674 setTimeout(function() {
2675 panoshow' . $id . '.loadScene("' . $default_scene . '");
2676 window.dispatchEvent(new Event("resize"));
2677 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2678 jQuery("#controls' . $id . '").css("bottom", "55px");
2679 }
2680 else {
2681 jQuery("#controls' . $id . '").css("bottom", "5px");
2682 }
2683 }, 200);
2684 });';
2685 return $html;
2686 }
2687 }
2688