PluginProbe
WpStream – Live Streaming, Video on Demand, Pay Per View / 4.14.1
WpStream – Live Streaming, Video on Demand, Pay Per View v4.14.1
4.14.1 4.14.0 4.13.2 4.13.1 4.13 4.12.5 4.12.4 4.12.3 4.12.2 4.12.1 4.12 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5 4.5.1 4.5.11 4.5.11.1 4.5.11.2 4.5.11.4 4.5.11.5 4.5.11.6 All 181 releases
wpstream / includes / class-wpstream-playback-presentation.php

class-wpstream-playback-presentation.php in WpStream – Live Streaming, Video on Demand, Pay Per View 4.14.1, at includes/class-wpstream-playback-presentation.php

1,422 lines 79.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Playback Presentation Module.
4 *
5 * Owns source resolution, engine selection, required assets, and the trusted
6 * viewer-facing HTML for already-approved Streaming Content.
7 *
8 * @package Wpstream
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 class Wpstream_Playback_Presentation {
16 /** @var Wpstream Main plugin instance. */
17 private $main;
18
19 /** @var Wpstream_Playback_Session Playback-session owner used by hosted players. */
20 private $playback_session;
21
22 /** @param Wpstream $main Main plugin instance. */
23 public function __construct( $main ) {
24 $this->main = $main;
25 $this->playback_session = new Wpstream_Playback_Session();
26 }
27
28 /**
29 * Render approved Streaming Content in the requested presentation mode.
30 *
31 * Entitlement is decided by the caller; this method only presents. Two
32 * filters and one action surround the render so themes and add-ons can
33 * change what is shown without forking the player templates.
34 *
35 * @param int $content_id Streaming Content post ID.
36 * @param string $intent auto, live, vod, or trailer-only.
37 * @return string Trusted player markup with dynamic values escaped by context.
38 */
39 public function render( $content_id, $intent = 'auto' ) {
40 $content_id = absint( $content_id );
41 $intent = strtolower( trim( (string) $intent ) );
42 if ( $content_id && 'auto' === $intent ) {
43 $intent = $this->classify( $content_id );
44 }
45 if ( $content_id ) {
46 /**
47 * Re-route the resolved presentation intent for one piece of content.
48 *
49 * Runs after `auto` has been classified, so callbacks always see a
50 * concrete intent. Returning a value outside live / vod / trailer-only
51 * renders nothing. Typical use: a soft paywall that forces
52 * `trailer-only` for guests.
53 *
54 * @since 4.14.0
55 *
56 * @param string $intent live, vod, or trailer-only.
57 * @param int $content_id Streaming Content post ID.
58 */
59 $intent = (string) apply_filters( 'wpstream_player_render_intent', $intent, $content_id );
60 }
61 if ( ! $content_id || ! in_array( $intent, array( 'live', 'vod', 'trailer-only' ), true ) ) {
62 return '';
63 }
64
65 // Render the chosen presentation into a buffer.
66 ob_start();
67 if ( 'live' === $intent ) {
68 $this->wpstream_live_event_player( $content_id );
69 } elseif ( 'vod' === $intent ) {
70 $this->wpstream_video_on_demand_player( $content_id );
71 } else {
72 $this->wpstream_video_on_demand_player_only_trailer( $content_id );
73 }
74 $html = (string) ob_get_clean();
75
76 /**
77 * Fires once after the player markup for one piece of content was built.
78 *
79 * @since 4.14.0
80 *
81 * @param int $content_id Streaming Content post ID.
82 * @param string $intent live, vod, or trailer-only.
83 * @param string $engine `iframe` (hosted player) or `videojs` (legacy in-page
84 * player) — a label derived from the embed-URL meta the
85 * renderers branch on, not a report of what was emitted.
86 */
87 do_action( 'wpstream_player_rendered', $content_id, $intent, $this->engine( $content_id, $intent ) );
88
89 /**
90 * Filter the complete player markup before it is returned to the caller.
91 *
92 * The markup is already escaped; callbacks own the escaping of anything
93 * they add. Entitlement was decided before this point — the filter cannot
94 * grant access, only change how approved content is presented.
95 *
96 * @since 4.14.0
97 *
98 * @param string $html Trusted player markup.
99 * @param int $content_id Streaming Content post ID.
100 * @param string $intent live, vod, or trailer-only.
101 */
102 return (string) apply_filters( 'wpstream_player_html', $html, $content_id, $intent );
103 }
104
105 /**
106 * Name the player engine the render used, derived from the same metadata
107 * the renderers branch on (an embed URL selects the hosted iframe player).
108 *
109 * @param int $content_id Streaming Content post ID.
110 * @param string $intent live, vod, or trailer-only.
111 * @return string iframe or videojs.
112 */
113 private function engine( $content_id, $intent ) {
114 if ( 'live' === $intent ) {
115 return get_post_meta( $content_id, 'embedUrl', true ) ? 'iframe' : 'videojs';
116 }
117 if ( 'vod' === $intent ) {
118 return get_post_meta( $content_id, 'wpstream_vod_embed_url', true ) ? 'iframe' : 'videojs';
119 }
120 return 'videojs';
121 }
122
123 /**
124 * Run the hosted-player iframe query through its filter without exposing
125 * the keys that are hashed into the embed key.
126 *
127 * `channel` / `video`, `embedKey`, `validatePlaybackSessionUrl`,
128 * `embedAncestor` and `encrypt` are signed together; a callback that changed
129 * one would produce a player that refuses to start. They are withheld from
130 * the filtered copy and re-applied afterwards, so callbacks can only touch
131 * presentation args (skin, poster, logo, autoplay, ...).
132 *
133 * @param string $hook `wpstream_live_iframe_query_args` or `wpstream_vod_iframe_query_args`.
134 * @param array $args Assembled query args (already filtered of empties).
135 * @param int $content_id Streaming Content post ID.
136 * @return array Query args with the signed keys restored.
137 */
138 private function filter_iframe_query_args( $hook, array $args, $content_id ) {
139 $signed_keys = array( 'channel', 'video', 'embedKey', 'validatePlaybackSessionUrl', 'embedAncestor', 'encrypt' );
140 $signed = array_intersect_key( $args, array_flip( $signed_keys ) );
141
142 /**
143 * Filter the hosted player's iframe query args.
144 *
145 * The signed keys (`channel` / `video`, `embedKey`,
146 * `validatePlaybackSessionUrl`, `embedAncestor`, `encrypt`) are not in
147 * `$args` and cannot be set: the canonical values are re-applied after
148 * the filter.
149 *
150 * @since 4.14.0
151 *
152 * @param array $args Presentation query args (skin, posterImage, logo*, autoplay, ...).
153 * @param int $content_id Streaming Content post ID.
154 */
155 $filtered = (array) apply_filters( $hook, array_diff_key( $args, $signed ), intval( $content_id ) );
156
157 // Drop any signed key the callback set (including ones absent above) and
158 // restore the canonical set, keeping the original key order.
159 $filtered = array_diff_key( $filtered, array_flip( $signed_keys ) );
160 $kept = $filtered + $signed;
161 return array_replace( array_intersect_key( $args, $kept ), $kept );
162 }
163
164 /**
165 * Resolve the poster image of a Streaming Content item.
166 *
167 * The single place the poster is read, for the hosted iframe and the
168 * legacy in-page player alike.
169 *
170 * @param int $content_id Streaming Content post ID.
171 * @param string $size Image size (`full` for the iframe, `small` for the legacy player).
172 * @return string Poster URL or an empty string.
173 */
174 public function poster_url( $content_id, $size = 'full' ) {
175 $url = (string) get_the_post_thumbnail_url( $content_id, $size );
176
177 /**
178 * Filter the player poster image URL.
179 *
180 * @since 4.14.0
181 *
182 * @param string $url Poster URL (empty when the item has no featured image).
183 * @param int $content_id Streaming Content post ID.
184 */
185 return (string) apply_filters( 'wpstream_player_poster_url', $url, intval( $content_id ) );
186 }
187
188 /**
189 * Resolve the pre-roll trailer of a Streaming Content item.
190 *
191 * The single place the plugin's players read the trailer; their trailer
192 * controls key off the returned URL being non-empty. (The bundled theme
193 * framework still reads the `video_trailer` meta for its own controls.)
194 *
195 * @param int $content_id Streaming Content post ID.
196 * @return string Trailer URL or an empty string when there is none.
197 */
198 public function trailer_url( $content_id ) {
199 $attachment_id = intval( get_post_meta( $content_id, 'video_trailer', true ) );
200 $url = $attachment_id ? (string) wp_get_attachment_url( $attachment_id ) : '';
201
202 /**
203 * Filter the pre-roll trailer URL.
204 *
205 * Returning an empty string removes the trailer; returning a URL adds
206 * one without a media-library attachment.
207 *
208 * @since 4.14.0
209 *
210 * @param string $url Trailer URL or empty.
211 * @param int $content_id Streaming Content post ID.
212 */
213 return (string) apply_filters( 'wpstream_trailer_url', $url, intval( $content_id ) );
214 }
215
216 /** @return array Legacy source-result shape. */
217 public function resolve_vod_source( $content_id ) {
218 return $this->wpstream_video_on_demand_player_uri_request( absint( $content_id ) );
219 }
220
221 /**
222 * Classify Streaming Content by its post type, product term or subscription flag.
223 *
224 * The single copy of the live-vs-VOD rule; other surfaces (My Account lists)
225 * compare against its return instead of re-reading the term and flag.
226 *
227 * @param int $content_id Post ID.
228 * @return string live, vod, or an empty string for unknown content.
229 */
230 public function classify( $content_id ) {
231 $post_type = get_post_type( $content_id );
232 if ( 'wpstream_product' === $post_type ) {
233 return 'live';
234 }
235 if ( 'wpstream_product_vod' === $post_type ) {
236 return 'vod';
237 }
238 if ( 'product' !== $post_type ) {
239 return '';
240 }
241
242 $product_type = wpstream_get_product_type_slug( $content_id );
243 if ( 'live_stream' === $product_type ) {
244 return 'live';
245 }
246 if ( 'video_on_demand' === $product_type ) {
247 return 'vod';
248 }
249 if ( 'subscription' === $product_type ) {
250 $subscription_mode = get_post_meta( $content_id, '_subscript_live_event', true );
251 if ( 'yes' === $subscription_mode ) {
252 return 'live';
253 }
254 if ( 'no' === $subscription_mode ) {
255 return 'vod';
256 }
257 }
258 return '';
259 }
260
261 public function remove_http($url) {
262 // Schemes we want to remove from the front of the string.
263 $disallowed = array('http://', 'https://');
264 foreach($disallowed as $d) {
265 // Only strip when the scheme is at the very start of the URL.
266 if(strpos($url, $d) === 0) {
267 return str_replace($d, '', $url);
268 }
269 }
270 // No leading scheme matched; return the URL as-is.
271 return $url;
272 }
273
274 private function wpstream_get_site_origin_for_embed() {
275 // Break the site home URL into scheme/host/port parts.
276 $parts = wp_parse_url( home_url() );
277 // Without a host there is no origin to build.
278 if ( empty( $parts['host'] ) ) {
279 return '';
280 }
281 // Default to https when the scheme is missing.
282 $scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : 'https://';
283 $host = $parts['host'];
284 // Include the port only when one is explicitly set.
285 $port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
286 // Reassemble scheme://host[:port].
287 return $scheme . $host . $port;
288 }
289
290 private function wpstream_get_player_embed_key_salt() {
291 // Hardcoded fallback salt; must match the player's env.js fallback.
292 $default_salt = 'EYjb84vNTXE85TfR';
293 // A site can override the salt via option.
294 $configured = trim( (string) get_option( 'wpstream_player_embed_key_salt', '' ) );
295 // Prefer the configured salt when present, else the default.
296 $base_salt = '' !== $configured ? $configured : $default_salt;
297
298 // Let integrators filter the final salt.
299 return (string) apply_filters( 'wpstream_player_embed_key_salt', $base_salt );
300 }
301
302 private function wpstream_generate_player_embed_key( $video, $validate_playback_session_url = '', $embed_ancestor = '', $encrypt_raw = '' ) {
303 // An empty media path produces no key.
304 $video = trim( (string) $video );
305 if ( '' === $video ) {
306 return '';
307 }
308
309 // Base input: "<video> <salt>". Optional parts are prepended in a fixed order.
310 $hash_input = $video . ' ' . $this->wpstream_get_player_embed_key_salt();
311 // Prepend the session-validation URL when supplied.
312 $validate_playback_session_url = trim( (string) $validate_playback_session_url );
313 if ( '' !== $validate_playback_session_url ) {
314 $hash_input = $validate_playback_session_url . ' ' . $hash_input;
315 }
316
317 // Prepend the allowed frame-ancestor origin when supplied.
318 $embed_ancestor = trim( (string) $embed_ancestor );
319 if ( '' !== $embed_ancestor ) {
320 $hash_input = $embed_ancestor . ' ' . $hash_input;
321 }
322
323 // Prepend the "crypt" marker when encryption is requested.
324 $encrypt_raw = strtolower( trim( (string) $encrypt_raw ) );
325 if ( in_array( $encrypt_raw, array( 'yes', 'true', '1' ), true ) ) {
326 $hash_input = 'crypt ' . $hash_input;
327 }
328
329 // MD5 the assembled input, then base64url-encode it (matching embed-key.js).
330 $binary_hash = md5( $hash_input, true );
331 $base64 = base64_encode( $binary_hash );
332
333 // URL-safe base64 without padding.
334 return rtrim( strtr( $base64, '+/', '-_' ), '=' );
335 }
336
337 public function wpstream_return_event_settings($product_id){
338 // Compatibility delegate: storage keys, legacy mode interpretation and
339 // one-time default snapshots belong to the Channel Settings Module.
340 $state = $this->main->channel_settings->read( (int) $product_id );
341 return $state['options'];
342 }
343
344 /**
345 * Deprecated misspelled alias of wpstream_return_event_settings().
346 *
347 * Kept because the method is public and may be called by external code.
348 *
349 * @deprecated 4.13.4 Use wpstream_return_event_settings() instead.
350 */
351 public function wpestream_return_event_settings($product_id){
352 return $this->wpstream_return_event_settings( $product_id );
353 }
354
355 public function wpstream_live_event_player($channel_id,$poster_show=''){
356 // Player dependencies (full stack plus the custom controls layer).
357 wpstream_enqueue_player_assets();
358 wp_enqueue_script( 'wpstream-player-controls' );
359
360 // Resolve the configured player skin/theme for this channel.
361 $player_theme = $this->wpstream_get_player_theme( $channel_id );
362 // Unique-ish id used to disambiguate multiple players on one page.
363 $now = time().rand(0,1000000);
364 $overlay_video_div_id = "random_id_".$now;
365 // print '<div id="'.esc_attr($overlay_video_div_id).'" class="vjs-title-overlay wpstream-video-title-overlay">'.esc_html__('Playing:','wpstream').' '.get_the_title($channel_id).'</div>';
366
367
368 // Poster thumbnail, streaming username, and autoplay default.
369 $poster_thumb = $this->poster_url( $channel_id, 'small' );
370 $usernamestream = esc_html ( wpstream_get_api_username() );
371 $autoplay = true;
372
373 // Effective per-event / global settings (autoplay, mute, view count, encryption...).
374 $event_settings = $this->wpstream_return_event_settings($channel_id);
375
376 // Live status via the channel-state owner (45s cache, Baker on miss,
377 // fresh-data meta sync — all inside Wpstream_Channel_State).
378 $state = Wpstream_Channel_State::for_channel( $channel_id, 'wpstream_live_event_player_note' );
379
380 // Playback URL / stats / chat URIs, populated only when the channel is
381 // live (playable: status active AND a playback URL present).
382 $hls_playback_url = '';
383 $live_conect_views = '';
384
385 if ( $state->is_live() ) {
386 $hls_playback_url = $state->playback_url();
387 $live_conect_views = $state->stats_url();
388 $chat_url = $state->chat_url();
389 }
390
391 // Optional pre-roll trailer; when present it replaces the poster.
392 $video_trailer = $this->trailer_url( $channel_id );
393 $has_trailer_class = '';
394 if ( '' !== $video_trailer ) {
395 $poster_data = ''; // cancel poster for theme
396 $has_trailer_class = 'wpstream_theme_player_has_trailer';
397 }
398
399 // Honour the "autoplay off" event setting.
400 if(isset($event_settings['autoplay']) && intval($event_settings['autoplay'])==0){
401 $autoplay=false;
402 }
403
404 // Bootstrap data-* values consumed by wpstream-player-bootstrap.js.
405 $bootstrap_is_muted = ( isset( $event_settings['mute'] ) && intval( $event_settings['mute'] ) === 1 );
406 // Content / stats / chat URIs passed to the player as data attributes.
407 $live_content_uri = isset( $hls_playback_url ) ? trim( $hls_playback_url ) : '';
408 $live_stats_uri = isset( $live_conect_views ) ? trim( $live_conect_views ) : '';
409 $live_chat_uri = isset( $chat_url ) ? trim( $chat_url ) : '';
410 // Element ids for the trailer play/mute/unmute buttons.
411 $play_trailer_button_element_id = 'wpstream_live_video_play_trailer_btn_' . $now;
412 $mute_trailer_button_element_id = 'wpstream_live_video_mute_trailer_btn_' . $now;
413 $unmute_trailer_button_element_id = 'wpstream_live_video_unmute_trailer_btn_' . $now;
414
415
416
417 // Nonce for the status-poll endpoint (used by the legacy player path).
418 $player_nonce = wp_create_nonce( 'wpstream_player_check_status_nonce' );
419
420 // New iframe player is keyed off the presence of an embedUrl meta value.
421 $live_channel_embed_url = get_post_meta( $channel_id, 'embedUrl', true );
422
423 // if embedUrl is set, we are using the new player
424 if ( !$live_channel_embed_url ) {
425 // Legacy Video.js path: emit the wrapper carrying all bootstrap data-* attributes.
426 echo '<div class="wpstream_live_player_wrapper function_wpstream_live_event_player" data-now="' . $now . '" data-me="' . esc_attr( $usernamestream ) . '" data-product-id="' . $channel_id . '" id="wpstream_live_player_wrapper' . $now . '" data-nonce="' . $player_nonce . '" data-wpstream-bootstrap="live" data-instance-id="wpstream-live-' . esc_attr( $now ) . '" data-video-element-id="' . esc_attr( $now ) . '" data-title-overlay-element-id="' . esc_attr( $overlay_video_div_id ) . '" data-content-url="' . esc_attr( $live_content_uri ) . '" data-stats-uri="' . esc_attr( $live_stats_uri ) . '" data-chat-url="' . esc_attr( $live_chat_uri ) . '" data-trailer-url="' . esc_attr( $video_trailer ) . '" data-autoplay="' . ( $autoplay ? '1' : '0' ) . '" data-muted="' . ( $bootstrap_is_muted ? '1' : '0' ) . '" data-play-trailer-button-element-id="' . esc_attr( $play_trailer_button_element_id ) . '" data-mute-trailer-button-element-id="' . esc_attr( $mute_trailer_button_element_id ) . '" data-unmute-trailer-button-element-id="' . esc_attr( $unmute_trailer_button_element_id ) . '" > ';
427
428 // Show the live viewer count unless the setting explicitly disables it.
429 $show_viewer_count = (
430 ( isset( $event_settings['view_count'] ) && intval( $event_settings['view_count'] ) == 1 )
431 || ! isset( $event_settings['view_count'] )
432 );
433
434 // Container the JS fills with the live viewer count.
435 echo '<div id="wpestream_live_counting" class="wpestream_live_counting" data-showviewercount="' . ( $show_viewer_count ? '1' : '0' ) . '"></div>';
436
437 // Show the "not live" overlay only while there is no playback URL yet.
438 $show_wpstream_not_live_mess = ' style="display:none;" ';
439 if ( trim( $hls_playback_url ) == '' ) {
440
441 $show_wpstream_not_live_mess = '';
442 }
443
444 // Default overlay carrying the configurable "we are not live" message.
445 $not_live_html = '<div class="wpstream_not_live_mess" ' . $show_wpstream_not_live_mess . ' style="display: none">
446 <div class="wpstream_not_live_mess_back"></div>
447 <div class="wpstream_not_live_mess_mess">' . esc_html( wpstream_not_live_message( $channel_id ) ) . '</div>
448 </div>';
449 // One-release shim: themes that still define the legacy section function
450 // keep winning until they move to the wpstream_not_live_html filter. The
451 // bundled framework defines it AND registers on the filter, so its section
452 // is built twice until the shim goes — harmless (state is cached).
453 if ( function_exists( 'wpstream_theme_not_live_section' ) ) {
454 $not_live_html = (string) wpstream_theme_not_live_section( $channel_id );
455 }
456
457 /**
458 * Filter the "not live" overlay rendered inside the in-page live player.
459 *
460 * The default markup is already escaped; callbacks own the escaping of
461 * anything they add. Replaces the `wpstream_theme_not_live_section()`
462 * theme sniff, which is honoured for one more release.
463 *
464 * @since 4.14.0
465 *
466 * @param string $html Overlay markup.
467 * @param int $channel_id Channel post ID.
468 * @param Wpstream_Channel_State $state Current channel state (status, playback URL).
469 */
470 print apply_filters( 'wpstream_not_live_html', $not_live_html, intval( $channel_id ), $state );
471
472
473 // Build the poster attribute; suppressed when caller passes 'no'.
474 $poster_data = '';
475 if ( '' !== $poster_thumb ) {
476 $poster_data = ' poster="' . esc_url( $poster_thumb ) . '" ';
477 }
478 if ( $poster_show == 'no' ) {
479 $poster_data = '';
480 }
481
482 // Start muted when the event setting requests it.
483 $is_muted = false;
484 if ( isset( $event_settings['mute'] ) && intval( $event_settings['mute'] ) == 1 ) {
485 $is_muted = true;
486 }
487 // override $is_muted and $autoplay here - for testing
488 // $autoplay = true;
489 // $is_muted = false;
490
491 // Translate the autoplay/muted booleans into HTML <video> attributes.
492 $autoplay_str = $autoplay ? 'autoplay' : '';
493 $is_muted_str = $is_muted ? 'muted' : '';
494
495 // override trailer url here - for testing
496 // $video_trailer = '';
497 // $video_trailer = '/wp-content/uploads/2023/10/production-ID_4608975.mp4';
498 // $video_trailer = '/wp-content/uploads/2023/10/ultrawide.mp4';
499
500
501 // Watermark/logo placement classes applied to the <video> element.
502 $player_logo_position_data = $this->wpstream_get_player_logo_data( $channel_id );
503 $player_logo_position = $player_logo_position_data['player_logo_position'];
504 $player_logo_position_class = $player_logo_position_data['player_logo_position_class'];
505 $player_logo_horizontal_position = $player_logo_position_data['player_logo_horizontal_position'];
506 // echo'
507 // <div class="wpstream-video-container">
508 // <div id="wpstream-pre-load-spinner" class="wpstream-pre-load-spinner"></div>
509 // <video id="wpstream-video'.$now.'" '.$poster_data.' class="video-js vjs-default-skin vjs-fluid vjs-wpstream ' . esc_attr($has_trailer_class) . ' ' . $player_theme . ' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" playsinline="true" '.$is_muted_str." ".$autoplay_str.'>
510 // </video>
511 // </div>';
512 // Pre-load spinner shown until the player is ready.
513 echo '<div class="wpstream-pre-load-spinner"></div>';
514 // The Video.js element itself, with skin/logo classes and autoplay/muted attributes.
515 echo '
516 <video id="wpstream-video' . $now . '" ' . $poster_data . ' class="video-js vjs-default-skin vjs-fluid vjs-wpstream ' . esc_attr( $has_trailer_class ) . ' ' . $player_theme . ' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" playsinline="true" ' . $is_muted_str . " " . $autoplay_str . '>
517
518 </video>';
519 // When a trailer exists, render its play/mute/unmute button controls (SVG icons).
520 if ( $video_trailer ) {
521 print '<div class="wpstream_theme_trailer_wrapper">';
522 print '<div id="' . esc_attr( $play_trailer_button_element_id ) . '" style="display: none;" class="wpstream_video_on_demand_play_trailer">
523 <svg width="30" height="24" viewBox="0 0 30 24" fill="none" xmlns="http://www.w3.org/2000/svg">
524 <path fill-rule="evenodd" clip-rule="evenodd" d="M26.6667 1.5H3.33337C2.50495 1.5 1.83337 2.17157 1.83337 3V21C1.83337 21.8284 2.50495 22.5 3.33338 22.5H26.6667C27.4951 22.5 28.1667 21.8284 28.1667 21V3C28.1667 2.17157 27.4951 1.5 26.6667 1.5ZM3.33337 0C1.67652 0 0.333374 1.34315 0.333374 3V21C0.333374 22.6569 1.67652 24 3.33338 24H26.6667C28.3236 24 29.6667 22.6569 29.6667 21V3C29.6667 1.34315 28.3236 0 26.6667 0H3.33337ZM4.83337 4C4.55723 4 4.33337 4.22386 4.33337 4.5V6.16667C4.33337 6.44281 4.55723 6.66667 4.83337 6.66667H6.50004C6.77618 6.66667 7.00004 6.44281 7.00004 6.16667V4.5C7.00004 4.22386 6.77618 4 6.50004 4H4.83337ZM23.5 4C23.2239 4 23 4.22386 23 4.5V6.16667C23 6.44281 23.2239 6.66667 23.5 6.66667H25.1667C25.4428 6.66667 25.6667 6.44281 25.6667 6.16667V4.5C25.6667 4.22386 25.4428 4 25.1667 4H23.5ZM4.33337 11.167C4.33337 10.8909 4.55723 10.667 4.83337 10.667H6.50004C6.77618 10.667 7.00004 10.8909 7.00004 11.167V12.8337C7.00004 13.1098 6.77618 13.3337 6.50004 13.3337H4.83337C4.55723 13.3337 4.33337 13.1098 4.33337 12.8337V11.167ZM23.5001 10.667C23.224 10.667 23.0001 10.8909 23.0001 11.167V12.8337C23.0001 13.1098 23.224 13.3337 23.5001 13.3337H25.1668C25.4429 13.3337 25.6668 13.1098 25.6668 12.8337V11.167C25.6668 10.8909 25.4429 10.667 25.1668 10.667H23.5001ZM4.33337 17.833C4.33337 17.5569 4.55723 17.333 4.83337 17.333H6.50004C6.77618 17.333 7.00004 17.5569 7.00004 17.833V19.4997C7.00004 19.7758 6.77618 19.9997 6.50004 19.9997H4.83337C4.55723 19.9997 4.33337 19.7758 4.33337 19.4997V17.833ZM23.5001 17.333C23.224 17.333 23.0001 17.5569 23.0001 17.833V19.4997C23.0001 19.7758 23.224 19.9997 23.5001 19.9997H25.1668C25.4429 19.9997 25.6668 19.7758 25.6668 19.4997V17.833C25.6668 17.5569 25.4429 17.333 25.1668 17.333H23.5001ZM19.0677 13.0997L13.4077 16.5087C13.0434 16.7281 12.6092 16.7094 12.2661 16.5091C11.9218 16.3081 11.6666 15.9224 11.6666 15.4086V8.59072C11.6666 8.07698 11.9218 7.69125 12.2661 7.49026C12.6092 7.28999 13.0434 7.27126 13.4077 7.49064L19.0677 10.8996C19.8663 11.3805 19.8663 12.6188 19.0677 13.0997Z"/>
525 </svg>
526 ' . esc_html__( 'Play Trailer', 'wpstream' ) . '</div>';
527 print '<div id="' . esc_attr( $mute_trailer_button_element_id ) . '" style="display: none;" class="wpstream_video_on_demand_mute_trailer">
528 <svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
529 <path fill-rule="evenodd" clip-rule="evenodd" d="M1.32143 10.0789H8.69499L18.8964 0L21.1428 0.921053V35.1316L18.8964 36L8.69499 25.8684H1.32143L0 24.5526V11.3947L1.32143 10.0789ZM10.175 23.6842L18.5 31.9474V4.10526L10.175 12.3158L9.24999 12.7105H2.64286V23.2368H9.24999L10.175 23.6842ZM37 17.9737C37.0069 22.2216 35.5329 26.3401 32.8295 29.6263L30.9478 27.7579C33.1613 24.9734 34.3629 21.5249 34.3571 17.9737C34.3571 14.2895 33.0885 10.8974 30.9637 8.21053L32.8454 6.34211C35.5382 9.62494 37.0062 13.735 37 17.9737ZM31.7143 17.9737C31.7193 20.8255 30.7895 23.6011 29.0661 25.8789L27.1738 23.9947C28.4127 22.2295 29.0752 20.1272 29.0714 17.9737C29.0751 15.8287 28.4174 13.7344 27.1871 11.9737L29.0793 10.0895C30.7338 12.2868 31.7143 15.0158 31.7143 17.9737ZM26.4286 17.9737C26.4286 19.4842 26.0057 20.8947 25.2657 22.0947L23.3126 20.1526C23.6249 19.4729 23.7876 18.7345 23.7899 17.9869C23.7922 17.2394 23.634 16.5001 23.3258 15.8184L25.2789 13.8737C26.0083 15.0684 26.4286 16.4737 26.4286 17.9737Z" fill="white"/>
530 </svg>
531
532 </div>';
533 print '<div id="' . esc_attr( $unmute_trailer_button_element_id ) . '" style="display: none;" class="wpstream_video_on_demand_unmute_trailer">
534 <svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
535 <path fill-rule="evenodd" clip-rule="evenodd" d="M1.15625 8.85688H7.60813L16.5344 0L18.5 0.809375V30.8719L16.5344 31.635L7.60813 22.7319H1.15625L0 21.5756V10.0131L1.15625 8.85688ZM8.90313 20.8125L16.1875 28.0738V3.6075L8.90313 10.8225L8.09375 11.1694H2.3125V20.4194H8.09375L8.90313 20.8125ZM30.5967 11.3127L32.2316 12.9477L28.2287 16.9506L32.2316 20.9559L30.5967 22.5908L26.5938 18.5856L22.5885 22.5908L20.9536 20.9559L24.9588 16.9506L20.9513 12.95L22.5862 11.3151L26.5938 15.3157L30.5967 11.3127Z" fill="white"/>
536 </svg>
537 </div>';
538 print '</div>';
539 }
540 // Close the legacy player wrapper.
541 print '</div>';
542
543
544 // Live player bootstrap is handled by wpstream-player-bootstrap.js.
545 } else {
546 // New iframe player path: assemble the /player/live iframe URL and query args.
547 $live_channel_frame_base = WPSTREAM_PLAYER . "/player/live?";
548 $live_channel_id = get_post_meta( $channel_id, 'channelId', true );
549
550 // Global channel options drive session encryption, domain lock, and autoplay.
551 $local_event_options = $this->wpstream_return_event_settings( $channel_id );
552 $wpstream_session_encryption = false;
553 $wpstream_live_channel_lock_to_website = false;
554 $wpstream_live_channel_autoplay = false;
555
556 // Session encryption on?
557 if ( isset( $local_event_options['ses_encrypt'] ) && intval( $local_event_options['ses_encrypt'] ) == 1 ) {
558 $wpstream_session_encryption = true;
559 }
560 // Lock playback to this website's origin?
561 if ( isset( $local_event_options['domain_lock'] ) && intval( $local_event_options['domain_lock'] ) == 1 ) {
562 $wpstream_live_channel_lock_to_website = true;
563 }
564 // Autoplay on?
565 if ( isset( $local_event_options['autoplay'] ) && intval( $local_event_options['autoplay'] ) == 1 ) {
566 $wpstream_live_channel_autoplay = true;
567 }
568
569 // When session encryption is on, build the validate-session URL and expose it to the controls JS.
570 // The URL is bound to this channel's post ID so a session issued for another
571 // product cannot pass verification here; no nonce is localized (the page may be
572 // served from a cache, and issuance is entitlement-gated server-side instead).
573 // Skipped entirely on non-publicly-routable hosts (local/dev sites): the
574 // presence backend could never fetch the URL, and advertising it makes the
575 // hosted player refuse to start (bootstrap 401 VALIDATION_UNAVAILABLE).
576 $wpstream_live_channel_validate_url = '';
577 if ( $wpstream_session_encryption && $this->playback_session->wpstream_playback_session_validation_available() ) {
578 $wpstream_live_channel_validate_url = esc_url_raw(
579 apply_filters(
580 'wpstream_live_channel_validate_playback_session_url',
581 $this->playback_session->wpstream_get_default_validate_playback_session_url( (int) $channel_id ),
582 $live_channel_id
583 )
584 );
585 wp_localize_script(
586 'wpstream-player-controls',
587 'wpstreamLiveIframeSessionApi',
588 array(
589 'requirePlaybackSession' => true,
590 'productId' => (int) $channel_id,
591 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
592 )
593 );
594 }
595
596 // Re-read the options and derive integer flags used in the iframe query args.
597 $local_event_options = $this->wpstream_return_event_settings( $channel_id );
598 $wpstream_live_channel_lock_to_website = 0;
599 $wpstream_live_channel_encrypt = 0;
600 $wpstream_live_channel_abr = 0;
601 $wpstream_live_channel_muted = 0;
602
603 // Domain lock flag.
604 if ( isset( $local_event_options['domain_lock'] ) && intval( $local_event_options['domain_lock'] ) == 1 ) {
605 $wpstream_live_channel_lock_to_website = 1;
606 }
607 // HLS encryption flag.
608 if ( isset( $local_event_options['encrypt'] ) && intval( $local_event_options['encrypt'] ) == 1 ) {
609 $wpstream_live_channel_encrypt = 1;
610 }
611 // Adaptive bitrate flag.
612 if ( isset( $local_event_options['adaptive_bitrate'] ) && intval( $local_event_options['adaptive_bitrate'] ) == 1 ) {
613 $wpstream_live_channel_abr = 1;
614 }
615 // Start-muted flag.
616 if ( isset( $local_event_options['mute'] ) && intval( $local_event_options['mute'] ) == 1 ) {
617 $wpstream_live_channel_muted = 1;
618 }
619
620 // Frame-ancestor origin, only set when playback is locked to this website.
621 $live_channel_embed_ancestor = '';
622 if ( $wpstream_live_channel_lock_to_website ) {
623 $live_channel_embed_ancestor = esc_url_raw(
624 apply_filters(
625 'wpstream_live_channel_embed_ancestor',
626 $this->wpstream_get_site_origin_for_embed(),
627 $live_channel_id
628 )
629 );
630 }
631
632 // Mint the embed key for the live channel from the same inputs as the iframe URL.
633 $wpstream_live_channel_embed_key = $this->wpstream_generate_player_embed_key(
634 $live_channel_id,
635 $wpstream_live_channel_validate_url,
636 $live_channel_embed_ancestor,
637 $wpstream_live_channel_encrypt ? 'yes' : ''
638 );
639 // Fall back to a stored embed_key if the generated one is empty.
640 if ( '' === $wpstream_live_channel_embed_key ) {
641 $wpstream_live_channel_embed_key = (string) get_post_meta( $channel_id, 'embed_key', true );
642 }
643 // Separate embed key for the trailer iframe.
644 $wpstream_live_channel_trailer_embed_key = $this->wpstream_generate_player_embed_key(
645 $video_trailer,
646 $wpstream_live_channel_validate_url,
647 $live_channel_embed_ancestor,
648 $wpstream_live_channel_encrypt ? 'yes' : ''
649 );
650
651 // Map the Video.js theme name to a bare skin slug the iframe player understands.
652 $wpstream_player_iframe_skin_slug = '';
653 if ( preg_match( '/^vjs-theme-(city|fantasy|forest|sea)$/', $player_theme, $vod_iframe_skin_m ) ) {
654 $wpstream_player_iframe_skin_slug = $vod_iframe_skin_m[1];
655 }
656 // Viewer-count badge on unless the setting disables it.
657 $wpstream_show_viewer_count = (
658 ( isset( $event_settings['view_count'] ) && intval( $event_settings['view_count'] ) == 1 )
659 || ! isset( $event_settings['view_count'] )
660 );
661
662 // Poster image and logo/watermark for the iframe query.
663 $wpstream_poster_image = $this->poster_url( $channel_id, 'full' );
664
665 $wpstream_player_logo_data = $this->wpstream_get_player_logo_data( $channel_id );
666 $wpstream_player_logo_image = $wpstream_player_logo_data['player_logo_src'];
667
668 // Assemble the live iframe query args, dropping null/empty values.
669 $live_channel_query_args = array_filter(
670 array(
671 'channel' => $live_channel_id,
672 'posterImage' => $wpstream_poster_image,
673 'embedKey' => $wpstream_live_channel_embed_key,
674 'autoplay' => $wpstream_live_channel_autoplay ? true : '',
675 'startMuted' => (bool) $wpstream_live_channel_muted,
676 'viewerCountBadge' => $wpstream_show_viewer_count ? true : '',
677 'encrypt' => $wpstream_live_channel_encrypt ? true : '',
678 'abr' => $wpstream_live_channel_abr ? true : '',
679 'validatePlaybackSessionUrl' => $wpstream_live_channel_validate_url,
680 'embedAncestor' => $live_channel_embed_ancestor,
681 'skin' => $wpstream_player_iframe_skin_slug,
682 'logoImage' => $wpstream_player_logo_image,
683 'logoPosition' => $wpstream_player_logo_image ? $wpstream_player_logo_data['player_logo_position'] : '',
684 'logoOpacity' => $wpstream_player_logo_image ? $wpstream_player_logo_data['player_logo_opacity'] : '',
685 'isThemeActive' => get_template() == 'hello-wpstream',
686 ),
687 static function ( $v ) {
688 return $v !== null && $v !== '';
689 }
690 );
691 /** This filter is documented in filter_iframe_query_args(). */
692 $live_channel_query_args = $this->filter_iframe_query_args( 'wpstream_live_iframe_query_args', $live_channel_query_args, $channel_id );
693
694 // Assemble the trailer iframe query args, dropping null/empty values.
695 $live_channel_trailer_iframe_query_args = array_filter(
696 array(
697 'video' => $video_trailer,
698 'embedKey' => $wpstream_live_channel_trailer_embed_key,
699 'skin' => $wpstream_player_iframe_skin_slug,
700 'startMuted' => (bool) $wpstream_live_channel_muted,
701 'encrypt' => $wpstream_live_channel_encrypt ? true : '',
702 'validatePlaybackSessionUrl' => $wpstream_live_channel_validate_url,
703 'embedAncestor' => $live_channel_embed_ancestor,
704 ),
705 static function ( $v ) {
706 return $v !== null && $v !== '';
707 }
708 );
709
710 // Emit the content iframe pointing at /player/live with the assembled query.
711 echo '<div class="wpstream_player_iframe_wrap">';
712 echo '<iframe id="playerFrame"
713 class="wpstream_live_channel_iframe"
714 title="' . esc_attr__( 'Embedded content', 'wpstream' ) . '"
715 src="' . esc_url( add_query_arg( $live_channel_query_args, $live_channel_frame_base ) ) . '"
716 data-wpstream-frame-role="content"
717 allowfullscreen
718 allow="autoplay; fullscreen">
719 </iframe>';
720 // Add the hidden trailer iframe only when a trailer exists and the WpStream theme is active.
721 if ( '' !== $video_trailer && get_template() === 'hello-wpstream' ) {
722 echo '<iframe id="playerFrameTrailer"
723 class="wpstream_live_channel_iframe wpstream_live_channel_iframe_trailer"
724 title="' . esc_attr__( 'Embedded trailer content', 'wpstream' ) . '"
725 src="' . esc_url( add_query_arg( $live_channel_trailer_iframe_query_args, WPSTREAM_PLAYER . "/player/vod?" ) ) . '"
726 data-wpstream-frame-role="trailer"
727 allowfullscreen
728 allow="autoplay; fullscreen"
729 style="display:none;"
730 aria-hidden="true"
731 tabindex="-1">
732 </iframe>';
733 }
734 echo '</div>';
735
736 }
737
738 }
739
740 /**
741 * Resolve the HLS manifest URL for a VOD file.
742 *
743 * Baker's video/info answer is cached per video name for 5 minutes; hit or
744 * miss, the DRM key and embed data it carries are stored on the requesting
745 * product. Returns '' when nothing playable is available.
746 *
747 * @param string $video_name Storage name of the video.
748 * @param int $product_id Post the video belongs to.
749 * @return string HLS URL or ''.
750 */
751 public function wpstream_request_video_on_demand_hls_player($video_name,$product_id){
752 // No video name means nothing to request.
753 if($video_name==''){
754 return '';
755 }
756
757 // Baker's video/info answer is cached per video for 5 minutes; Baker is asked only on a miss.
758 $transient_name = 'wpstream_video_on_demand_' . $video_name;
759 $curl_response_decoded = get_transient( $transient_name );
760
761 if ( ! is_array( $curl_response_decoded ) ) {
762 // Endpoint for video info; the auth token is injected by the transport.
763 $url = 'video/info';
764
765 // Resolve the canonical site origin. Request context is not
766 // authoritative here: wp-cli, cron and TLS-terminating proxies
767 // can make is_ssl() false for a site configured as HTTPS.
768 $local_event_options = get_option('wpstream_user_streaming_global_channel_options') ;
769 $domain = parse_url ( get_site_url() );
770 $domain_scheme = isset( $domain['scheme'] ) ? $domain['scheme'] : 'http';
771
772 // With VOD domain-lock on, restrict CORS to this site's origin; otherwise allow any.
773 $wpstream_vod_domain_lock = intval( get_option('wpstream_vod_domain_lock','') ) ;
774 $corsorigin='*';
775 if($wpstream_vod_domain_lock !== 0 ){
776 $corsorigin=$domain_scheme.'://'.$domain['host'];
777 }
778
779 // Request HLS encryption when the VOD encrypt option is enabled.
780 $is_encrypt="false";
781 $wpstream_vod_encrypt = intval( get_option('wpstream_vod_encrypt','') ) ;
782 if( intval( $wpstream_vod_encrypt ) ==1 ){
783 $is_encrypt="true";
784 }
785
786 // Prefix the encryption-key requests route back through this site (?wpstream_voddrm=).
787 $hlsKeysUrlPrefix = get_site_url().'?wpstream_voddrm=';
788 $encrypt = $is_encrypt;
789 $debugDrm = false;
790
791 // Body of the API request.
792 $curl_post_fields=array(
793 'name' => $video_name,
794 'corsOrigin' => $corsorigin,
795 'encryptHls' => $encrypt,
796 'hlsKeysUrlPrefix' => $hlsKeysUrlPrefix,
797 'debugDrm' => $debugDrm,
798 'embed' => true,
799 );
800
801
802 // Perform the API call; a WP_Error (not-connected included) means no playable URL.
803 $curl_response_decoded = $this->main->wpstream_live_connection->authorized_request( $url, $curl_post_fields );
804 if ( is_wp_error( $curl_response_decoded ) ) {
805 $curl_response_decoded = array();
806 }
807
808 // API reported failure: nothing to cache, no URL to return.
809 if ( empty( $curl_response_decoded['success'] ) || empty( $curl_response_decoded['hlsUrl'] ) ) {
810 return '';
811 }
812
813 /**
814 * Filter how long a VOD's HLS answer from the cloud is cached, in seconds.
815 *
816 * Clamped to [1, 3600]. The transient name and payload are frozen.
817 *
818 * @since 4.14.0
819 *
820 * @param int $seconds Lifetime; default 300.
821 */
822 $ttl = wpstream_tunable( 'wpstream_vod_hls_cache_ttl', 300, HOUR_IN_SECONDS );
823 // Cache the whole answer so a later hit can apply the same product meta below.
824 set_transient( $transient_name, $curl_response_decoded, $ttl );
825 }
826
827 // Store or clear the HLS decryption key/index on the requesting product.
828 if( isset($curl_response_decoded['hlsDecryptionKey']) && isset($curl_response_decoded['hlsDecryptionKeyIndex']) ){
829 update_post_meta($product_id,'hlsDecryptionKey',$curl_response_decoded['hlsDecryptionKey']);
830 update_post_meta($product_id,'hlsDecryptionKeyIndex',$curl_response_decoded['hlsDecryptionKeyIndex']);
831 }else{
832 delete_post_meta($product_id,'hlsDecryptionKey');
833 delete_post_meta($product_id,'hlsDecryptionKeyIndex');
834 }
835
836 // When the API returns new-player embed data, persist it as post meta.
837 if ( isset( $curl_response_decoded['video'] ) &&
838 isset( $curl_response_decoded['embedKey'] ) &&
839 isset( $curl_response_decoded['embedUrl'] )
840 ) {
841 update_post_meta( $product_id, 'wpstream_vod_video_data', trim( (string) $curl_response_decoded['video'] ) );
842 update_post_meta( $product_id, 'wpstream_vod_embed_key', trim( (string) $curl_response_decoded['embedKey'] ) );
843 update_post_meta( $product_id, 'wpstream_vod_embed_url', trim( (string) $curl_response_decoded['embedUrl'] ) );
844 }
845
846 return $curl_response_decoded['hlsUrl'];
847
848 }
849
850 public function wpstream_get_player_theme( $channel_id = null ) {
851 // Configured theme name and whether this is a streamify (basic) user.
852 $player_theme = get_option('wpstream_video_player_theme');
853 $is_streamify_user = $this->wpstream_is_streamify_user( $channel_id );
854 // Apply the theme only for non-streamify users with a theme set.
855 if ( !empty($player_theme) && !$is_streamify_user ) {
856 $this->wpstream_enqueue_player_theme_style( $player_theme );
857 return 'vjs-theme-' . $player_theme;
858 }
859
860 // No theme (default skin or streamify user).
861 return '';
862 }
863
864 public function wpstream_enqueue_player_theme_style( $player_theme ) {
865 // The 'default' skin ships with Video.js, so only load extra theme CSS.
866 // Only skins we bundle may be enqueued; anything else falls back to default.
867 $bundled_themes = array( 'city', 'forest', 'fantasy', 'sea' );
868 if ( in_array( $player_theme, $bundled_themes, true ) ) {
869 wp_enqueue_style( 'videojs-theme-' . $player_theme, plugin_dir_url( __DIR__ ) . 'public/css/vendor/videojs-theme-' . $player_theme . '.css', array(), '1.0.1' );
870 }
871 }
872
873 private function wpstream_get_player_logo_data( $product_id ): array {
874 // Configured logo corner, e.g. "top-left".
875 $player_logo_position = (string) get_option( 'wpstream_player_logo_position', 'top-left' );
876
877 // Logo image URL and its opacity (stored as a percentage, returned as 0-1).
878 $player_logo_src = $this->wpstream_get_video_player_logo( $product_id );
879 $player_logo_opacity = intval( esc_html( get_option('wpstream_player_logo_opacity','100') ) ) / 100;
880
881 /**
882 * Filter the player logo placement.
883 *
884 * @since 4.14.0
885 *
886 * @param array $data {
887 * @type string $player_logo_src Logo URL (already through `wpstream_player_logo`).
888 * @type string $player_logo_position Corner, e.g. `top-left`.
889 * @type float $player_logo_opacity 0..1.
890 * }
891 * @param int $product_id Streaming Content post ID.
892 */
893 $data = (array) apply_filters(
894 'wpstream_player_logo_data',
895 array(
896 'player_logo_src' => $player_logo_src,
897 'player_logo_position' => $player_logo_position,
898 'player_logo_opacity' => $player_logo_opacity,
899 ),
900 intval( $product_id )
901 );
902
903 // Derive the legacy player's corner classes from the (possibly filtered) position.
904 $position_class = '';
905 $horizontal = '';
906 $parts = explode( '-', (string) ( $data['player_logo_position'] ?? '' ) );
907 if ( isset( $parts[0], $parts[1] ) ) {
908 $position_class = 'logo-' . $parts[0];
909 $horizontal = 'logo-' . $parts[1];
910 }
911
912 // Bundle the logo image, corner, derived classes, and opacity.
913 return array(
914 'player_logo_src' => (string) ( $data['player_logo_src'] ?? '' ),
915 'player_logo_position' => (string) ( $data['player_logo_position'] ?? '' ),
916 'player_logo_position_class' => $position_class,
917 'player_logo_horizontal_position' => $horizontal,
918 'player_logo_opacity' => (float) ( $data['player_logo_opacity'] ?? 1 ),
919 );
920 }
921
922 public function wpstream_video_on_demand_player_uri_request($product_id){
923 // Base Video.js data-setup attribute.
924 $wpstream_data_setup = ' data-setup="{}" ';
925
926 /* free_video_type
927 * 1 - free live channel
928 * 2 - free video encrypted
929 * 3 - free video -not encrypted
930 */
931
932 // Post type and the stored "free video type" classification.
933 $post_type = get_post_type($product_id);
934 $free_video_type = intval( get_post_meta($product_id, 'wpstream_product_type', true));
935 $video_path_final = '';
936 $video_type = '';
937 if( ( $post_type =='wpstream_product_vod' && $free_video_type==2 ) || get_post_type($product_id)=='product' ){
938
939 /*
940 * IF vide is encrypted- readed from vod,streaner
941 *
942 */
943
944 // Encrypted / paid VOD: fetch an HLS manifest from the cloud API.
945 $video_type = 'application/x-mpegURL';
946 $video_path = get_post_meta($product_id,'_movie_url',true);
947 if(get_post_type($product_id)=='wpstream_product_vod'){
948 // Free-VOD post type stores the source under a different meta key.
949 $video_path = esc_html(get_post_meta($product_id, 'wpstream_free_video', true));
950 }
951 $video_path_final = $this->wpstream_request_video_on_demand_hls_player($video_path,$product_id);
952
953
954 }else if( $post_type =='wpstream_product_vod' && $free_video_type==3 ){
955
956 /* Video is unecrypted - read from local or youtube / vimeo
957 */
958
959 // Unencrypted VOD: use the stored external URL and load the YouTube tech.
960 $video_type = 'video/mp4';
961 $video_path_final=esc_html(get_post_meta($product_id, 'wpstream_free_video_external', true));
962 wp_enqueue_script('youtube.min');
963 }
964
965 // Package the resolved source details for the caller.
966 $return_array = array();
967 $return_array['video_path_final'] = $video_path_final;
968 $return_array['wpstream_data_setup']= $wpstream_data_setup;
969 $return_array['video_type'] = $video_type;
970 $return_array['free_video_type'] = $free_video_type;
971 $return_array['post_type'] = $post_type ;
972 return $return_array;
973 }
974
975 public function wpstream_video_on_demand_player($product_id){
976 // Player dependencies (full stack plus the custom controls layer).
977 wpstream_enqueue_player_assets();
978 wp_enqueue_script( 'wpstream-player-controls' );
979
980 // Resolve the configured skin/theme and the VOD source details.
981 $player_theme = $this->wpstream_get_player_theme( $product_id );
982
983 $uri_details = $this->wpstream_video_on_demand_player_uri_request($product_id);
984 $video_path_final = $uri_details['video_path_final'];
985 $wpstream_data_setup = $uri_details['wpstream_data_setup'];
986 $video_type = $uri_details['video_type'];
987 // Unique-ish id for this player instance.
988 $now = time().rand(0,1000000);
989
990 // Render the optional title overlay via the theme action hook.
991 $overlay_video_div_id = "random_id_".$now;
992 $this->wpstream_render_vod_title_overlay(
993 $overlay_video_div_id,
994 get_the_title($product_id)
995 );
996
997 // Poster thumbnail and streaming username.
998 $poster_thumb = $this->poster_url( $product_id, 'small' );
999 $usernamestream = esc_html ( wpstream_get_api_username() );
1000
1001 // DRM decryption key/index stored earlier by the HLS request.
1002 $hlsDecryptionKey = get_post_meta($product_id,'hlsDecryptionKey',true);
1003 $hlsDecryptionKeyIndex = get_post_meta($product_id,'hlsDecryptionKeyIndex',true);
1004
1005
1006 // Quota/pack data used below to decide whether streaming is allowed.
1007 $pack = $this->main->quota_manager->get_live_quota_data( 'wpstream_video_on_demand_player' );
1008
1009
1010
1011 // Optional pre-roll trailer (one accessor, filterable).
1012 $video_trailer = $this->trailer_url( $product_id );
1013
1014 // override trailer setup here (for testing)
1015 // $video_trailer = '/wp-content/uploads/2023/10/production-ID_4608975.mp4';
1016 // $video_trailer = '/wp-content/uploads/2023/10/ultrawide.mp4';
1017
1018 // If the video is self hosted or external, we should let the user see it
1019 // (type 3 = unencrypted; bypasses the quota check below).
1020 $video_type = intval( get_post_meta($product_id, 'wpstream_product_type', true));
1021
1022
1023 // Render the player only when the quota allows VOD streaming, or the video is unencrypted (type 3).
1024 if ( $this->main->quota_manager->can_stream_vod( $pack ) || $video_type === 3 ) {
1025
1026 if($video_path_final==''){
1027 // Empty source: show a "missing video" notice, except for unencrypted VODs.
1028 if( $uri_details['post_type']=='wpstream_product_vod' && $uri_details['free_video_type']==3 ){
1029 }else{
1030 print '<div class="wpstream_vod_notice">This video does not exist or it has been deleted!</div>';
1031 }
1032
1033 }
1034
1035 // Autoplay/mute defaults, overridden by the VOD options below.
1036 // TODO (crerem) populate these from VOD settings
1037 $autoplay = false;
1038 $muted = false;
1039
1040 // Start muted when the VOD "start muted" option is on.
1041 $wpstream_vod_start_muted = intval ( get_option('wpstream_vod_start_muted','') );
1042 if($wpstream_vod_start_muted===1){
1043 $muted=true;
1044 }
1045 // Autoplay when the VOD autoplay option is on.
1046 $wpstream_vod_autoplay = intval ( get_option('wpstream_vod_autoplay','') );
1047 if($wpstream_vod_autoplay===1){
1048 $autoplay=true;
1049 }
1050
1051 // Poster attribute; dropped in favour of a trailer when one exists.
1052 $poster_data = 'poster="'.esc_url($poster_thumb).'"';
1053 $has_trailer_class='';
1054 if('' !== $video_trailer){
1055 $poster_data=''; // cancel poster for theme
1056 $has_trailer_class='wpstream_theme_player_has_trailer';
1057 }
1058
1059 // Logo/watermark image, position, and opacity for the player.
1060 $player_logo_data = $this->wpstream_get_player_logo_data( $product_id );
1061 $player_logo_image = $player_logo_data['player_logo_src'];
1062 $player_logo_position = $player_logo_data['player_logo_position'];
1063 $player_logo_position_class = $player_logo_data['player_logo_position_class'];
1064 $player_logo_horizontal_position = $player_logo_data['player_logo_horizontal_position'];
1065 $player_logo_opacity = $player_logo_data['player_logo_opacity'];
1066
1067 // Captions file and the trailer/video control button element ids.
1068 $captionsUrl = get_post_meta( $product_id, 'wpstream_closed_captions_file', true );
1069 $play_trailer_button_element_id = '' !== $video_trailer ? 'wpstream_video_on_demand_play_trailer_btn_' . $now : '';
1070 $mute_trailer_button_element_id = '' !== $video_trailer ? 'wpstream_video_on_demand_mute_trailer_btn_' . $now : '';
1071 $unmute_trailer_button_element_id = '' !== $video_trailer ? 'wpstream_video_on_demand_unmute_trailer_btn_' . $now : '';
1072 $play_video_button_element_id = '' !== $video_trailer ? 'wpstream_video_on_demand_play_video_btn_' . $now : '';
1073
1074 // VOD encryption and domain-lock options.
1075 $wpstream_vod_encrypt = intval( get_option( 'wpstream_vod_encrypt', '' ) );
1076 $wpstream_vod_lock_to_website = intval( get_option( 'wpstream_vod_domain_lock', '' ) );
1077
1078 // Session encryption flag from the global channel options.
1079 $wpstream_session_encryption = 0;
1080 $local_event_options = get_option('wpstream_user_streaming_global_channel_options') ;
1081 if(isset($local_event_options['ses_encrypt']) && intval($local_event_options['ses_encrypt'])==1 ) {
1082 $wpstream_session_encryption = 1;
1083 }
1084
1085 /**
1086 * Presence bootstrap expects validatePlaybackSessionUrl as an absolute HTTP(S) URL that returns JSON { success: true } when the playback session is valid — not the literals "true"/"false".
1087 * Embed keys must be minted with the same inputs as the iframe query (video, validatePlaybackSessionUrl, embedAncestor, encrypt) per player embed-key.js.
1088 *
1089 * @see WPStream player embed-key.js and session-validation.js (verifyPlaybackSessionUrl).
1090 */
1091 // With session encryption on, build the validate-session URL and expose the session API to the controls JS.
1092 // The URL is bound to this product's ID so a session issued for another
1093 // product cannot pass verification here; no nonce is localized (the page may
1094 // be served from a cache, and issuance is entitlement-gated server-side instead).
1095 // Skipped entirely on non-publicly-routable hosts (local/dev sites): the
1096 // presence backend could never fetch the URL, and advertising it makes the
1097 // hosted player refuse to start (bootstrap 401 VALIDATION_UNAVAILABLE).
1098 $vod_validate_url = '';
1099 if ( $wpstream_session_encryption && $this->playback_session->wpstream_playback_session_validation_available() ) {
1100 $vod_validate_url = esc_url_raw(
1101 apply_filters(
1102 'wpstream_vod_validate_playback_session_url',
1103 $this->playback_session->wpstream_get_default_validate_playback_session_url( (int) $product_id ),
1104 $product_id
1105 )
1106 );
1107 wp_localize_script(
1108 'wpstream-player-controls',
1109 'wpstreamVodIframeSessionApi',
1110 array(
1111 'requirePlaybackSession' => true,
1112 'productId' => (int) $product_id,
1113 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
1114 )
1115 );
1116 }
1117
1118 // Frame-ancestor origin, only when VOD playback is locked to this website.
1119 $vod_embed_ancestor = '';
1120 if ( $wpstream_vod_lock_to_website ) {
1121 $vod_embed_ancestor = esc_url_raw(
1122 apply_filters(
1123 'wpstream_vod_embed_ancestor',
1124 $this->wpstream_get_site_origin_for_embed(),
1125 $product_id
1126 )
1127 );
1128 }
1129
1130 // if there's "wpstream_vod_embed_url" set, it means that we use the new player
1131 $wpstream_vod_embed_url = get_post_meta( $product_id, 'wpstream_vod_embed_url', true );
1132
1133 // New-player iframe base URL and the stored video identifier.
1134 $vod_iframe_base = WPSTREAM_PLAYER . "/player/vod?";
1135 $vod_iframe_video_raw = (string) get_post_meta( $product_id, 'wpstream_vod_video_data', true );
1136 $vod_iframe_video = trim( $vod_iframe_video_raw );
1137 // Strip a surrounding pair of single/double quotes from the stored value.
1138 if ( strlen( $vod_iframe_video ) >= 2 ) {
1139 $first_char = $vod_iframe_video[0];
1140 $last_char = substr( $vod_iframe_video, -1 );
1141 $is_wrapped_in_double_quotes = '"' === $first_char && '"' === $last_char;
1142 $is_wrapped_in_single_quotes = "'" === $first_char && "'" === $last_char;
1143 if ( $is_wrapped_in_double_quotes || $is_wrapped_in_single_quotes ) {
1144 $vod_iframe_video = substr( $vod_iframe_video, 1, -1 );
1145 }
1146 }
1147 // Mint the VOD embed key from the same inputs used in the iframe URL.
1148 $vod_iframe_embed_key = $this->wpstream_generate_player_embed_key(
1149 $vod_iframe_video,
1150 $vod_validate_url,
1151 $vod_embed_ancestor,
1152 $wpstream_vod_encrypt ? 'yes' : ''
1153 );
1154 // Fall back to a stored embed key when the generated one is empty.
1155 if ( '' === $vod_iframe_embed_key ) {
1156 $vod_iframe_embed_key = (string) get_post_meta( $product_id, 'wpstream_vod_embed_key', true );
1157 }
1158 // Separate embed key for the trailer iframe.
1159 $vod_iframe_trailer_embed_key = $this->wpstream_generate_player_embed_key(
1160 $video_trailer,
1161 $vod_validate_url,
1162 $vod_embed_ancestor,
1163 $wpstream_vod_encrypt ? 'yes' : ''
1164 );
1165
1166 // Poster image and active theme (used for the isThemeActive flag).
1167 $vod_poster_image = $this->poster_url( $product_id, 'full' );
1168 $current_active_theme = wp_get_theme();
1169
1170 // Map the Video.js theme name to a bare skin slug for the iframe player.
1171 $vod_iframe_skin_slug = '';
1172 if ( preg_match( '/^vjs-theme-(city|fantasy|forest|sea)$/', $player_theme, $vod_iframe_skin_m ) ) {
1173 $vod_iframe_skin_slug = $vod_iframe_skin_m[1];
1174 }
1175
1176 // Assemble the VOD iframe query args, dropping null/empty values.
1177 $vod_iframe_query_args = array_filter(
1178 array(
1179 'video' => $vod_iframe_video,
1180 'posterImage' => $vod_poster_image,
1181 'embedKey' => $vod_iframe_embed_key,
1182 // Honour the VOD "autoplay" option in the hosted player (it starts muted, as browsers require).
1183 'autoplay' => $autoplay ? '1' : '',
1184 'startMuted' => $muted ? '1' : '',
1185 'skin' => $vod_iframe_skin_slug,
1186 'encrypt' => $wpstream_vod_encrypt ? 'yes' : '',
1187 'validatePlaybackSessionUrl' => $vod_validate_url,
1188 'embedAncestor' => $vod_embed_ancestor,
1189 'logoImage' => $player_logo_image,
1190 'logoPosition' => $player_logo_image ? $player_logo_position : '',
1191 'logoOpacity' => $player_logo_image ? $player_logo_opacity : '',
1192 'isThemeActive' => $current_active_theme->get('Name') === 'Hello WPStream',
1193 ),
1194 static function ( $v ) {
1195 return $v !== null && $v !== '';
1196 }
1197 );
1198 /** This filter is documented in filter_iframe_query_args(). */
1199 $vod_iframe_query_args = $this->filter_iframe_query_args( 'wpstream_vod_iframe_query_args', $vod_iframe_query_args, $product_id );
1200
1201 // Assemble the trailer iframe query args, dropping null/empty values.
1202 $vod_trailer_iframe_query_args = array_filter(
1203 array(
1204 'video' => $video_trailer,
1205 'embedKey' => $vod_iframe_trailer_embed_key,
1206 'skin' => $vod_iframe_skin_slug,
1207 'startMuted' => $muted ? '1' : '',
1208 'encrypt' => $wpstream_vod_encrypt ? 'yes' : '',
1209 'validatePlaybackSessionUrl' => $vod_validate_url,
1210 'embedAncestor' => $vod_embed_ancestor,
1211 ),
1212 static function ( $v ) {
1213 return $v !== null && $v !== '';
1214 }
1215 );
1216
1217 // New iframe player when an embed URL exists; otherwise the legacy Video.js element.
1218 if ( $wpstream_vod_embed_url ) {
1219 // Emit the content iframe pointing at /player/vod with the assembled query.
1220 echo '<div class="wpstream_player_iframe_wrap">';
1221 echo '<iframe id="playerFrame"
1222 class="wpstream_video_on_demand_iframe"
1223 title="' . esc_attr__( 'Embedded content', 'wpstream' ) . '"
1224 src="' . esc_url( add_query_arg( $vod_iframe_query_args, $vod_iframe_base ) ) . '"
1225 data-wpstream-frame-role="content"
1226 allowfullscreen
1227 allow="autoplay; fullscreen">
1228 </iframe>';
1229 // Add the hidden trailer iframe only when a trailer exists and the WpStream theme is active.
1230 if ( '' !== $video_trailer && get_template() == 'hello-wpstream' ) {
1231 echo '<iframe id="playerFrameTrailer"
1232 class="wpstream_video_on_demand_iframe wpstream_video_on_demand_iframe_trailer"
1233 title="' . esc_attr__( 'Embedded trailer content', 'wpstream' ) . '"
1234 src="' . esc_url( add_query_arg( $vod_trailer_iframe_query_args, $vod_iframe_base ) ) . '"
1235 data-wpstream-frame-role="trailer"
1236 allowfullscreen
1237 allow="autoplay; fullscreen"
1238 style="display:none;"
1239 aria-hidden="true"
1240 tabindex="-1">
1241 </iframe>';
1242 }
1243 echo '</div>';
1244 } else {
1245 // Legacy Video.js <video> element carrying every bootstrap data-* attribute.
1246 echo '<video id="wpstream-video-vod-'.$now.'" class="'.esc_attr($has_trailer_class).' video-js vjs-default-skin vjs-fluid kuk wpstream_video_on_demand vjs-wpstream ' . $player_theme .' ' . $player_logo_position_class . ' ' . $player_logo_horizontal_position . '" data-me="'.esc_attr($usernamestream).'" data-product-id="'.$product_id.'" data-wpstream-bootstrap="vod" data-instance-id="wpstream-vod-'.esc_attr( $now ).'" data-video-element-id="wpstream-video-vod-'.esc_attr( $now ).'" data-title-overlay-element-id="'.esc_attr( $overlay_video_div_id ).'" data-video-url="'.esc_attr( $video_path_final ).'" data-trailer-url="'.esc_attr( $video_trailer ).'" data-autoplay="'. ( $autoplay ? '1' : '0' ) .'" data-muted="'. ( $muted ? '1' : '0' ) .'" data-captions-url="'.esc_attr( $captionsUrl ).'" data-play-trailer-button-element-id="'.esc_attr( $play_trailer_button_element_id ).'" data-mute-trailer-button-element-id="'.esc_attr( $mute_trailer_button_element_id ).'" data-unmute-trailer-button-element-id="'.esc_attr( $unmute_trailer_button_element_id ).'" data-play-video-button-element-id="'.esc_attr( $play_video_button_element_id ).'" data-player-logo-image="'.esc_attr( $player_logo_image ).'" data-player-logo-position="'.esc_attr( $player_logo_position ).'" data-player-logo-opacity="'.esc_attr( $player_logo_opacity ).'" data-player-logo-width="100" data-player-logo-height="auto" data-player-logo-padding="10" playsinline preload="auto"
1247 '. $poster_data.' '.$wpstream_data_setup.'>
1248 <p class="vjs-no-js">
1249 To view this video please enable JavaScript, and consider upgrading to a web browser that
1250 <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
1251 </p>
1252 </video>';
1253
1254 // When a trailer exists, render its play-trailer / play-video / mute / unmute controls (SVG icons).
1255 if('' !== $video_trailer){
1256 print '<div class="wpstream_theme_trailer_wrapper">';
1257 print '<div id="'.esc_attr( $play_trailer_button_element_id ).'" class="wpstream_video_on_demand_play_trailer">
1258 <svg width="30" height="24" viewBox="0 0 30 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1259 <path fill-rule="evenodd" clip-rule="evenodd" d="M26.6667 1.5H3.33337C2.50495 1.5 1.83337 2.17157 1.83337 3V21C1.83337 21.8284 2.50495 22.5 3.33338 22.5H26.6667C27.4951 22.5 28.1667 21.8284 28.1667 21V3C28.1667 2.17157 27.4951 1.5 26.6667 1.5ZM3.33337 0C1.67652 0 0.333374 1.34315 0.333374 3V21C0.333374 22.6569 1.67652 24 3.33338 24H26.6667C28.3236 24 29.6667 22.6569 29.6667 21V3C29.6667 1.34315 28.3236 0 26.6667 0H3.33337ZM4.83337 4C4.55723 4 4.33337 4.22386 4.33337 4.5V6.16667C4.33337 6.44281 4.55723 6.66667 4.83337 6.66667H6.50004C6.77618 6.66667 7.00004 6.44281 7.00004 6.16667V4.5C7.00004 4.22386 6.77618 4 6.50004 4H4.83337ZM23.5 4C23.2239 4 23 4.22386 23 4.5V6.16667C23 6.44281 23.2239 6.66667 23.5 6.66667H25.1667C25.4428 6.66667 25.6667 6.44281 25.6667 6.16667V4.5C25.6667 4.22386 25.4428 4 25.1667 4H23.5ZM4.33337 11.167C4.33337 10.8909 4.55723 10.667 4.83337 10.667H6.50004C6.77618 10.667 7.00004 10.8909 7.00004 11.167V12.8337C7.00004 13.1098 6.77618 13.3337 6.50004 13.3337H4.83337C4.55723 13.3337 4.33337 13.1098 4.33337 12.8337V11.167ZM23.5001 10.667C23.224 10.667 23.0001 10.8909 23.0001 11.167V12.8337C23.0001 13.1098 23.224 13.3337 23.5001 13.3337H25.1668C25.4429 13.3337 25.6668 13.1098 25.6668 12.8337V11.167C25.6668 10.8909 25.4429 10.667 25.1668 10.667H23.5001ZM4.33337 17.833C4.33337 17.5569 4.55723 17.333 4.83337 17.333H6.50004C6.77618 17.333 7.00004 17.5569 7.00004 17.833V19.4997C7.00004 19.7758 6.77618 19.9997 6.50004 19.9997H4.83337C4.55723 19.9997 4.33337 19.7758 4.33337 19.4997V17.833ZM23.5001 17.333C23.224 17.333 23.0001 17.5569 23.0001 17.833V19.4997C23.0001 19.7758 23.224 19.9997 23.5001 19.9997H25.1668C25.4429 19.9997 25.6668 19.7758 25.6668 19.4997V17.833C25.6668 17.5569 25.4429 17.333 25.1668 17.333H23.5001ZM19.0677 13.0997L13.4077 16.5087C13.0434 16.7281 12.6092 16.7094 12.2661 16.5091C11.9218 16.3081 11.6666 15.9224 11.6666 15.4086V8.59072C11.6666 8.07698 11.9218 7.69125 12.2661 7.49026C12.6092 7.28999 13.0434 7.27126 13.4077 7.49064L19.0677 10.8996C19.8663 11.3805 19.8663 12.6188 19.0677 13.0997Z"/>
1260 </svg>
1261 '.esc_html__('Play Trailer','wpstream').'</div>';
1262
1263 print '<div class="wpstream_video_on_demand_play_video_wrapper" id="'.esc_attr( $play_video_button_element_id ).'" >
1264 <div class="wpstream_video_on_demand_play_video">
1265 <svg width="29" height="30" viewBox="0 0 29 30" fill="none" xmlns="http://www.w3.org/2000/svg">
1266 <path fill-rule="evenodd" clip-rule="evenodd" d="M6.1808 28.9035L26.274 18.1652C29.1087 16.6503 29.1087 12.7497 26.274 11.2348L6.1808 0.496557C4.88769 -0.194506 3.34623 -0.1355 2.1283 0.495357C0.906043 1.12846 1.0095e-06 2.34351 9.38766e-07 3.96179L0 25.4382C-7.07369e-08 27.0565 0.906042 28.2715 2.1283 28.9046C3.34622 29.5355 4.88769 29.5945 6.1808 28.9035ZM24.8221 13.8026C25.5742 14.2045 25.5742 15.1955 24.8221 15.5974L4.72891 26.3356C3.94628 26.7539 3.01386 26.2165 3.01386 25.4382L3.01386 3.96179C3.01386 3.18347 3.94628 2.6461 4.72891 3.06436L24.8221 13.8026Z" fill="#F1F1F1"/>
1267 </svg>
1268 </div>
1269 '.esc_html__('Play Video','wpstream').'
1270 </div>';
1271
1272
1273
1274 print '<div id="'.esc_attr( $mute_trailer_button_element_id ).'" class="wpstream_video_on_demand_mute_trailer">
1275
1276 <svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
1277 <path fill-rule="evenodd" clip-rule="evenodd" d="M1.32143 10.0789H8.69499L18.8964 0L21.1428 0.921053V35.1316L18.8964 36L8.69499 25.8684H1.32143L0 24.5526V11.3947L1.32143 10.0789ZM10.175 23.6842L18.5 31.9474V4.10526L10.175 12.3158L9.24999 12.7105H2.64286V23.2368H9.24999L10.175 23.6842ZM37 17.9737C37.0069 22.2216 35.5329 26.3401 32.8295 29.6263L30.9478 27.7579C33.1613 24.9734 34.3629 21.5249 34.3571 17.9737C34.3571 14.2895 33.0885 10.8974 30.9637 8.21053L32.8454 6.34211C35.5382 9.62494 37.0062 13.735 37 17.9737ZM31.7143 17.9737C31.7193 20.8255 30.7895 23.6011 29.0661 25.8789L27.1738 23.9947C28.4127 22.2295 29.0752 20.1272 29.0714 17.9737C29.0751 15.8287 28.4174 13.7344 27.1871 11.9737L29.0793 10.0895C30.7338 12.2868 31.7143 15.0158 31.7143 17.9737ZM26.4286 17.9737C26.4286 19.4842 26.0057 20.8947 25.2657 22.0947L23.3126 20.1526C23.6249 19.4729 23.7876 18.7345 23.7899 17.9869C23.7922 17.2394 23.634 16.5001 23.3258 15.8184L25.2789 13.8737C26.0083 15.0684 26.4286 16.4737 26.4286 17.9737Z" fill="white"/>
1278 </svg>
1279 </div>';
1280 print '<div id="'.esc_attr( $unmute_trailer_button_element_id ).'" class="wpstream_video_on_demand_unmute_trailer">
1281 <svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
1282 <path fill-rule="evenodd" clip-rule="evenodd" d="M1.15625 8.85688H7.60813L16.5344 0L18.5 0.809375V30.8719L16.5344 31.635L7.60813 22.7319H1.15625L0 21.5756V10.0131L1.15625 8.85688ZM8.90313 20.8125L16.1875 28.0738V3.6075L8.90313 10.8225L8.09375 11.1694H2.3125V20.4194H8.09375L8.90313 20.8125ZM30.5967 11.3127L32.2316 12.9477L28.2287 16.9506L32.2316 20.9559L30.5967 22.5908L26.5938 18.5856L22.5885 22.5908L20.9536 20.9559L24.9588 16.9506L20.9513 12.95L22.5862 11.3151L26.5938 15.3157L30.5967 11.3127Z" fill="white"/>
1283 </svg>
1284
1285 </div>';
1286 print '</div>';
1287 }
1288 }
1289 }else{
1290 // Quota exhausted for an encrypted VOD: show an "insufficient resources" notice.
1291 print '<div class="wpstream_insuficent_res">'.esc_html__('Insufficient resources to stream this title','wpstream').'</div>';
1292 }
1293
1294 }
1295
1296 public function wpstream_video_on_demand_player_only_trailer($product_id){
1297 // Player dependencies (scripts + styles).
1298 wpstream_enqueue_player_assets();
1299
1300 // Skin/theme, unique id, and the optional title overlay.
1301 $player_theme = $this->wpstream_get_player_theme();
1302 $now = time().rand(0,1000000);
1303 $overlay_video_div_id = "random_id_".$now;
1304 $this->wpstream_render_vod_title_overlay(
1305 $overlay_video_div_id,
1306 get_the_title($product_id)
1307 );
1308
1309
1310
1311 // Poster thumbnail and streaming username.
1312 $poster_thumb = $this->poster_url( $product_id, 'small' );
1313 $usernamestream = esc_html ( wpstream_get_api_username() );
1314
1315
1316 // Optional pre-roll trailer (one accessor, filterable).
1317 $video_trailer = $this->trailer_url( $product_id );
1318
1319
1320 // Autoplay/mute from the VOD options.
1321 $autoplay = false;
1322 $muted = false;
1323
1324 if( intval ( get_option('wpstream_vod_start_muted','') ) === 1){
1325 $muted = true;
1326 }
1327 if( intval ( get_option('wpstream_vod_autoplay','') ) === 1 ){
1328 $autoplay = true;
1329 }
1330 // No main video source (trailer only); captions and control ids.
1331 $video_path_final='';
1332 $has_trailer_class='wpstream_theme_player_has_trailer';
1333 $captions_url = get_post_meta( $product_id, 'wpstream_closed_captions_file', true );
1334 $play_trailer_button_element_id = '' !== $video_trailer ? 'wpstream_video_on_demand_play_trailer_btn_' . $now : '';
1335 $mute_trailer_button_element_id = '' !== $video_trailer ? 'wpstream_video_on_demand_mute_trailer_btn_' . $now : '';
1336 $unmute_trailer_button_element_id = '' !== $video_trailer ? 'wpstream_video_on_demand_unmute_trailer_btn_' . $now : '';
1337
1338 // Video.js element with an empty data-video-url (trailer supplied via data-trailer-url).
1339 echo '<video id="wpstream-video-vod-'.$now.'" class="video-js vjs-default-skin vjs-fluid kuk wpstream_video_on_demand vjs-wpstream '.esc_attr( $has_trailer_class ).' ' . $player_theme . '" data-me="'.esc_attr($usernamestream).'" data-product-id="'.$product_id.'" data-wpstream-bootstrap="vod" data-instance-id="wpstream-vod-trailer-'.esc_attr( $now ).'" data-video-element-id="wpstream-video-vod-'.esc_attr( $now ).'" data-title-overlay-element-id="'.esc_attr( $overlay_video_div_id ).'" data-video-url="" data-trailer-url="'.esc_attr( $video_trailer ).'" data-autoplay="'. ( $autoplay ? '1' : '0' ) .'" data-muted="'. ( $muted ? '1' : '0' ) .'" data-captions-url="'.esc_attr( $captions_url ).'" data-play-trailer-button-element-id="'.esc_attr( $play_trailer_button_element_id ).'" data-mute-trailer-button-element-id="'.esc_attr( $mute_trailer_button_element_id ).'" data-unmute-trailer-button-element-id="'.esc_attr( $unmute_trailer_button_element_id ).'" playsinline preload="auto"
1340 >
1341 <p class="vjs-no-js">
1342 To view this video please enable JavaScript, and consider upgrading to a web browser that
1343 <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
1344 </p>
1345 </video>';
1346
1347 // Render the trailer play/mute/unmute controls (SVG icons) when a trailer exists.
1348 if('' !== $video_trailer){
1349 print '<div class="wpstream_theme_trailer_wrapper">';
1350 print '<div id="'.esc_attr( $play_trailer_button_element_id ).'" class="wpstream_video_on_demand_play_trailer">
1351 <svg width="30" height="24" viewBox="0 0 30 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1352 <path fill-rule="evenodd" clip-rule="evenodd" d="M26.6667 1.5H3.33337C2.50495 1.5 1.83337 2.17157 1.83337 3V21C1.83337 21.8284 2.50495 22.5 3.33338 22.5H26.6667C27.4951 22.5 28.1667 21.8284 28.1667 21V3C28.1667 2.17157 27.4951 1.5 26.6667 1.5ZM3.33337 0C1.67652 0 0.333374 1.34315 0.333374 3V21C0.333374 22.6569 1.67652 24 3.33338 24H26.6667C28.3236 24 29.6667 22.6569 29.6667 21V3C29.6667 1.34315 28.3236 0 26.6667 0H3.33337ZM4.83337 4C4.55723 4 4.33337 4.22386 4.33337 4.5V6.16667C4.33337 6.44281 4.55723 6.66667 4.83337 6.66667H6.50004C6.77618 6.66667 7.00004 6.44281 7.00004 6.16667V4.5C7.00004 4.22386 6.77618 4 6.50004 4H4.83337ZM23.5 4C23.2239 4 23 4.22386 23 4.5V6.16667C23 6.44281 23.2239 6.66667 23.5 6.66667H25.1667C25.4428 6.66667 25.6667 6.44281 25.6667 6.16667V4.5C25.6667 4.22386 25.4428 4 25.1667 4H23.5ZM4.33337 11.167C4.33337 10.8909 4.55723 10.667 4.83337 10.667H6.50004C6.77618 10.667 7.00004 10.8909 7.00004 11.167V12.8337C7.00004 13.1098 6.77618 13.3337 6.50004 13.3337H4.83337C4.55723 13.3337 4.33337 13.1098 4.33337 12.8337V11.167ZM23.5001 10.667C23.224 10.667 23.0001 10.8909 23.0001 11.167V12.8337C23.0001 13.1098 23.224 13.3337 23.5001 13.3337H25.1668C25.4429 13.3337 25.6668 13.1098 25.6668 12.8337V11.167C25.6668 10.8909 25.4429 10.667 25.1668 10.667H23.5001ZM4.33337 17.833C4.33337 17.5569 4.55723 17.333 4.83337 17.333H6.50004C6.77618 17.333 7.00004 17.5569 7.00004 17.833V19.4997C7.00004 19.7758 6.77618 19.9997 6.50004 19.9997H4.83337C4.55723 19.9997 4.33337 19.7758 4.33337 19.4997V17.833ZM23.5001 17.333C23.224 17.333 23.0001 17.5569 23.0001 17.833V19.4997C23.0001 19.7758 23.224 19.9997 23.5001 19.9997H25.1668C25.4429 19.9997 25.6668 19.7758 25.6668 19.4997V17.833C25.6668 17.5569 25.4429 17.333 25.1668 17.333H23.5001ZM19.0677 13.0997L13.4077 16.5087C13.0434 16.7281 12.6092 16.7094 12.2661 16.5091C11.9218 16.3081 11.6666 15.9224 11.6666 15.4086V8.59072C11.6666 8.07698 11.9218 7.69125 12.2661 7.49026C12.6092 7.28999 13.0434 7.27126 13.4077 7.49064L19.0677 10.8996C19.8663 11.3805 19.8663 12.6188 19.0677 13.0997Z"/>
1353 </svg>
1354 '.esc_html__('Play Trailer','wpstream').'
1355 </div>';
1356 print '<div id="'.esc_attr( $mute_trailer_button_element_id ).'" class="wpstream_video_on_demand_mute_trailer">
1357 <svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
1358 <path fill-rule="evenodd" clip-rule="evenodd" d="M1.32143 10.0789H8.69499L18.8964 0L21.1428 0.921053V35.1316L18.8964 36L8.69499 25.8684H1.32143L0 24.5526V11.3947L1.32143 10.0789ZM10.175 23.6842L18.5 31.9474V4.10526L10.175 12.3158L9.24999 12.7105H2.64286V23.2368H9.24999L10.175 23.6842ZM37 17.9737C37.0069 22.2216 35.5329 26.3401 32.8295 29.6263L30.9478 27.7579C33.1613 24.9734 34.3629 21.5249 34.3571 17.9737C34.3571 14.2895 33.0885 10.8974 30.9637 8.21053L32.8454 6.34211C35.5382 9.62494 37.0062 13.735 37 17.9737ZM31.7143 17.9737C31.7193 20.8255 30.7895 23.6011 29.0661 25.8789L27.1738 23.9947C28.4127 22.2295 29.0752 20.1272 29.0714 17.9737C29.0751 15.8287 28.4174 13.7344 27.1871 11.9737L29.0793 10.0895C30.7338 12.2868 31.7143 15.0158 31.7143 17.9737ZM26.4286 17.9737C26.4286 19.4842 26.0057 20.8947 25.2657 22.0947L23.3126 20.1526C23.6249 19.4729 23.7876 18.7345 23.7899 17.9869C23.7922 17.2394 23.634 16.5001 23.3258 15.8184L25.2789 13.8737C26.0083 15.0684 26.4286 16.4737 26.4286 17.9737Z" fill="white"/>
1359 </svg>
1360
1361
1362 </div>';
1363 print '<div id="'.esc_attr( $unmute_trailer_button_element_id ).'" class="wpstream_video_on_demand_unmute_trailer">
1364 <svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
1365 <path fill-rule="evenodd" clip-rule="evenodd" d="M1.15625 8.85688H7.60813L16.5344 0L18.5 0.809375V30.8719L16.5344 31.635L7.60813 22.7319H1.15625L0 21.5756V10.0131L1.15625 8.85688ZM8.90313 20.8125L16.1875 28.0738V3.6075L8.90313 10.8225L8.09375 11.1694H2.3125V20.4194H8.09375L8.90313 20.8125ZM30.5967 11.3127L32.2316 12.9477L28.2287 16.9506L32.2316 20.9559L30.5967 22.5908L26.5938 18.5856L22.5885 22.5908L20.9536 20.9559L24.9588 16.9506L20.9513 12.95L22.5862 11.3151L26.5938 15.3157L30.5967 11.3127Z" fill="white"/>
1366 </svg>
1367
1368 </div>';
1369 print '</div>';
1370 }
1371 else {
1372 //just show the poster or don't show anything; no player needed
1373 }
1374
1375 }
1376
1377 private function wpstream_render_vod_title_overlay( $overlay_id, $title_text ) {
1378 // Only fire when a theme/plugin has registered the overlay renderer.
1379 if ( has_action( 'wpstream_vod_title_overlay' ) ) {
1380 do_action(
1381 'wpstream_vod_title_overlay',
1382 $overlay_id,
1383 $title_text,
1384 esc_html__( 'Playing:', 'wpstream' )
1385 );
1386 }
1387 }
1388
1389 public function wpstream_get_video_player_logo( $product_id ) {
1390 // Streamify (basic) users always get the bundled WpStream watermark.
1391 $is_streamify_user = $this->wpstream_is_streamify_user( $product_id );
1392 if ( $is_streamify_user ) {
1393 return WPSTREAM_PLUGIN_DIR_URL . 'img/wpstream-symbol-large.png';
1394 }
1395
1396 // Otherwise the site-configured logo, which an integration may replace.
1397 $logo = (string) get_option( 'wpstream_player_logo', '' );
1398
1399 /**
1400 * Filter the player logo / watermark image URL.
1401 *
1402 * Not applied for basic-plan (streamify) channels, whose bundled
1403 * watermark is part of the plan.
1404 *
1405 * @since 4.14.0
1406 *
1407 * @param string $logo Logo URL or empty for none.
1408 * @param int $product_id Streaming Content post ID.
1409 */
1410 return (string) apply_filters( 'wpstream_player_logo', $logo, intval( $product_id ) );
1411 }
1412
1413 public function wpstream_is_streamify_user( $channel_id ) {
1414 // The 'basicStreaming' meta flag marks streamify (basic) channels.
1415 $is_basic_streaming = get_post_meta( $channel_id, 'basicStreaming', true );
1416 if ( $is_basic_streaming === '1' ) {
1417 return true;
1418 }
1419 return false;
1420 }
1421 }
1422