| 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 |
|