PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.1.3
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.1.3
9.1.3 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 All 222 releases
wpvr / legacy / admin / helpers / class-wpvr-format.php

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

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