PluginProbe
FV Player 8 / 8.1
FV Player 8 v8.1
trunk 8.0.18 8.0.19 8.0.20 8.0.21 8.0.25 8.0.27 8.1 8.1.3
fv-player / models / video-encoder / video-encoder.php

video-encoder.php in FV Player 8 8.1, at models/video-encoder/video-encoder.php

1,125 lines 39.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 abstract class FV_Player_Video_Encoder {
4 private
5 $encoder_id = '', // used for unique action names and asset names (CSS, JS)
6 // examples: coconut, bunny_stream, dos ...
7 $encoder_wp_url_slug = '', // used in all links that will point to the list of this encoder jobs
8 // examples: fv_player_coconut, fv_player_bunny_stream ...
9 $encoder_name = '', // used to display name of the service where appropriate (mostly information DIVs in a HTML output)
10 // examples: Coconut, Bunny Stream ...
11 $instance = null, // self-explanatory
12 $admin_page = false, // will be set to a real admin submenu page object once created
13 $browser_inc_file = '', // the full inclusion path for this Encoder's browser PHP backend file, so we can include_once() it
14 $use_wp_list_table; // allow descendants to decide if use wp list table
15
16 // variables to override or access from outside of the base class
17 protected
18 $version = 'latest',
19 $license_key = false;
20
21 public
22 $table_name = 'fv_player_encoding_jobs'; // table in which encoding jobs are stored
23
24 public function _get_instance() {
25 return $this->instance;
26 }
27
28 public function get_version() {
29 return $this->version;
30 }
31
32 public function get_table_name() {
33 return $this->table_name;
34 }
35
36 protected function __construct( $encoder_id, $encoder_name, $encoder_wp_url_slug, $browser_inc_file = '', $use_wp_list_table = true ) {
37 global $wpdb;
38
39 if ( !$encoder_id ) {
40 throw new Exception('Extending encoder class did not provide an encoder ID!');
41 }
42
43 if ( !$encoder_name ) {
44 throw new Exception('Extending encoder class did not provide an encoder name!');
45 }
46
47 if ( !$encoder_wp_url_slug ) {
48 throw new Exception('Extending encoder class did not provide an encoder URL slug!');
49 }
50
51 $this->encoder_id = $encoder_id;
52 $this->encoder_name = $encoder_name;
53 $this->encoder_wp_url_slug = $encoder_wp_url_slug;
54
55 // table names always start on WP prefix, so add that here for our table name here
56 $this->table_name = $wpdb->prefix . $this->table_name;
57 $this->browser_inc_file = $browser_inc_file;
58 $this->use_wp_list_table = $use_wp_list_table;
59
60 add_action('init', array( $this, 'email_notification' ), 7 );
61
62 if( is_admin() ) {
63 add_action( 'admin_menu', array($this, 'admin_menu'), 11 );
64
65 add_filter( 'fv_player_conf_defaults', array( $this, 'default_settings' ), 10, 2 );
66
67 $version = get_option( 'fv_player_' . $this->encoder_id . '_ver' );
68 if( $this->version != $version ) {
69 update_option( 'fv_player_' . $this->encoder_id . '_ver', $this->version );
70
71 // This is where FV Player will set any default settings.
72 // We have to do this again as we only init this plugin on plugins_loaded after $fv_fp has been created with the default settings initialized.
73 global $fv_fp;
74 if( !empty($fv_fp) && method_exists( $fv_fp, '_get_conf' ) ) {
75 $fv_fp->_get_conf();
76 }
77
78 $this->plugin_update_database();
79 }
80
81 add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
82
83 add_action( 'wp_ajax_fv_player_' . $this->encoder_id .'_submit', array( $this, 'ajax_fv_player_job_submit') );
84
85 add_action( 'wp_ajax_fv_player_' . $this->encoder_id .'_delete_job', array( $this, 'ajax_fv_player_delete_job') );
86
87 //add_action( 'plugins_loaded', array( $this, 'init_browser') );
88 // this file is actually only included after the 'plugins_loaded' action was fired, so let's run this method manually
89 $this->init_browser();
90
91 // we use a custom taxonomy to categorize the jobs
92 add_action( 'admin_init', array( $this, 'create_encoding_categories' ) );
93
94 // when a new encoding category gets added, we don't want it to show on top of the list
95 add_filter( 'wp_terms_checklist_args', array( $this, 'category_picker_args' ) );
96
97 // Periodically update jobs status when wp hearbeat is fired
98 add_filter( 'heartbeat_received', array( $this, 'heartbeat_check' ), 10, 3 );
99
100 // Editor enhancements to store job ID with video
101 add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_editor_scripts'));
102 add_action( 'fv_flowplayer_shortcode_editor_item_after', array( $this, 'shortcode_editor_item' ) );
103
104 add_filter('plugin_action_links', array( $this, 'admin_plugin_action_links' ), 10, 2);
105
106 $options = get_option( 'fvwpflowplayer' );
107 if( !empty($options[ $this->encoder_id ]) && !empty($options[ $this->encoder_id ]['license_key']) ) {
108 $this->license_key = $options[ $this->encoder_id ]['license_key'];
109 }
110
111 add_action( 'admin_notices', array( $this, 'admin_notices' ) );
112
113 add_action( 'fv_player_video_encoder_include_listing_lib', array( $this, 'include_listing_lib' ), 10, 0 );
114 }
115
116 add_action( 'fv_player_item', array( $this, 'check_playlist_video_is_processing' ) );
117 }
118
119 /**
120 * Includes a generic jobs listing library, so it can be used outside of this class,
121 * i.e. by extending Encoder classes, when needed.
122 */
123 public function include_listing_lib() {
124 require_once dirname( __FILE__ ) . '/class.fv-player-encoder-list-table.php';
125 }
126
127 /**
128 * Checks whether the video is being processed by the extending Encoder
129 * and if so, includes JS & CSS for that Encoder (+ global overlay CSS) on page, so the extending class
130 * can display overlays with error / progress messages.
131 *
132 * @param $item array The actual video item to check.
133 *
134 * @return array Returns an augmented video item data, if its source was found to be a video in encoding process.
135 */
136 public function check_playlist_video_is_processing( $item ) {
137 if ( is_array($item['sources']) ) {
138 foreach( $item['sources'] as $source ) {
139 if ( strpos($source['src'], $this->encoder_id . '_processing_' ) !== false ) {
140 $item['pending_encoding'] = true;
141
142 $job_id = explode( $this->encoder_id. '_processing_', $source['src'] );
143 if( !empty($job_id[1]) ) {
144 $job_id = $job_id[1];
145
146 $check = $this->update_temporary_job_src( false, $job_id );
147 if( !empty($check['progress']) && ( $check['status'] != 'error' ) ) {
148 $item['pending_encoding_progress'] = $check['progress'];
149 } else {
150 $item['pending_encoding_error'] = true;
151 }
152 }
153 }
154 }
155 }
156
157 return $item;
158 }
159
160 /**
161 * Enqueues a JS file for shortcode editor when needed on the backend pages.
162 *
163 * @param $page The identifier of a page we're currently viewing.
164 */
165 public function admin_enqueue_editor_scripts($page) {
166 if( $page == 'post.php' || $page == 'post-new.php' || $page == 'toplevel_page_fv_player' ) {
167
168 $file = $this->locate_script('shortcode-editor.js');
169 if( $file ) {
170 $handle = 'fvplayer-shortcode-editor-' . $this->encoder_id;
171 wp_enqueue_script( $handle, plugins_url( $file, $this->getFILE() ), array('jquery'), filemtime( dirname( $this->getFILE() ) . $file), true );
172 }
173
174 }
175 }
176
177 /**
178 * Periodically updates jobs status when wp hearbeat is fired.
179 *
180 * @param $response array The heartbeat response body which we're augmenting with our job data.
181 * @param $data array containing IDs of jobs pending encoding for the current extending Encoder class.
182 * @param $screen_id string ID of the page we're currently viewing.
183 *
184 * @return mixed
185 */
186 public function heartbeat_check( $response, $data, $screen_id ) {
187 if( strcmp( 'fv-player_page_' . $this->encoder_wp_url_slug, $screen_id ) == 0 ) {
188 if( isset($data[ $this->encoder_id . '_pending' ]) ) {
189 $ids = $data[ $this->encoder_id . '_pending' ];
190 $response[ $this->encoder_id . '_still_pending'] = $this->jobs_check(true); // update pending job in js
191 $rows_html = $this->get_updated_rows( $ids );
192 $response[ $this->encoder_id ] = $rows_html; // html for jobs
193 }
194 }
195
196 return $response;
197 }
198
199 function locate_script( $script ) {
200 $file = false;
201 if( file_exists( dirname( $this->getFILE() ) . '/../js/'.$this->encoder_id.'-'.$script ) ) {
202 $file = '/../js/'.$this->encoder_id.'-'.$script;
203 } else if( file_exists( dirname( $this->getFILE() ).'/js/'.$script ) ) {
204 $file = '/js/'.$script;
205 }
206 return $file;
207 }
208
209 /**
210 * Returns augmented arguments array for the category picker with the option for "checked_ontop" set to FALSE.
211 *
212 * @param $args The original arguments array for the category picker.
213 *
214 * @return array Returns augmented arguments array for the category picker with the option for "checked_ontop" set to FALSE.
215 */
216 function category_picker_args( $args ) {
217 if( !empty($_POST['action']) && strcmp( sanitize_key( $_POST['action'] ), 'add-fv_player_encoding_category') == 0 ) {
218 $args['checked_ontop'] = false;
219 }
220 return $args;
221 }
222
223 /**
224 * Creates encoding categories taxonomy.
225 */
226 function create_encoding_categories() {
227 register_taxonomy(
228 'fv_player_encoding_category',
229 'fv_player_encoding_job',
230 array(
231 'hierarchical' => true,
232 'rewrite' => false // we only need the category names
233 )
234 );
235 }
236
237 /**
238 * Adds FV Player admin menu item to show jobs for this Encoder.
239 */
240 function admin_menu(){
241 if( current_user_can('edit_posts') ) {
242
243 $title = $this->encoder_name . ( $this->is_configured() ? ' Jobs' : '' );
244
245 $this->admin_page = add_submenu_page( 'fv_player', $title, $title, 'edit_posts', $this->encoder_wp_url_slug, array( $this, 'tools_panel' ) );
246
247 if( $this->is_configured() ) {
248 add_action( 'load-'.$this->admin_page, array( $this, 'screen_options' ) );
249 //add_filter( 'manage_toplevel_page_fv_player_columns', array( $this, 'screen_columns' ) );
250 //add_filter( 'hidden_columns', array( $this, 'screen_columns_hidden' ), 10, 3 );
251 add_filter( 'set-screen-option', array($this, 'set_screen_option'), 10, 3);
252 }
253 }
254 }
255
256 /**
257 * Adds Settings or Finish Set-Up tab links on top of the Encoder's jobs listing page.
258 *
259 * @param $links array An array of existing tab links.
260 * @param $file string Filename in which we're calling this action.
261 *
262 * @return array Returns an array with new tab links added to it.
263 */
264 function admin_plugin_action_links($links, $file) {
265 if ( stripos( $file, 'fv-player-' . $this->encoder_id . '.php') !== false ) {
266 if ( $this->is_configured() ) {
267 $extra_link = '<a href="'.admin_url('admin.php?page=' . $this->encoder_wp_url_slug . '&panel=settings').'">Settings</a>';
268 } else {
269 $extra_link = '<a href="'.admin_url('admin.php?page=' . $this->encoder_wp_url_slug).'">Finish Set-Up</a>';
270 }
271 array_unshift($links, $extra_link);
272 }
273 return $links;
274 }
275
276 /**
277 * Ajax handler for deleting completed and errorred-out jobs
278 *
279 * @param int $_POST['id_row'] ID of the job row
280 *
281 * @return JSON Status message
282 */
283 function ajax_fv_player_delete_job() {
284 global $wpdb;
285
286 $id = absint( $_POST['id_row'] );
287
288 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv-player-encoder-delete-job-' . $id ) ) {
289 wp_send_json( array('error' => 'Bad nonce') );
290 }
291
292 if ( $wpdb->query( $wpdb->prepare("DELETE FROM `{$wpdb->prefix}fv_player_encoding_jobs` WHERE id = %d ", $id) ) ) {
293 wp_send_json( array('success' => 'Job deleted successfully') );
294 } else {
295 wp_send_json( array('error' => 'Error deleting row') );
296 }
297 }
298
299 /**
300 * Ajax handler for creation of new job.
301 *
302 * @param string $_POST['source'] Source file URL
303 * @param string $_POST['target'] Target folder on the target CDN
304 * @param string $_POST['encryption'] Should it encrypt the video?
305 *
306 * @return JSON New job table row HTML in html property and also error property if there is any error
307 */
308 function ajax_fv_player_job_submit() {
309 global $wpdb;
310
311 // TODO: update JS to generate correct nonce ID (it was coconut_expert_nonce before)
312 if(
313 defined('DOING_AJAX') &&
314 ( !isset( $_POST['nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv_player_' . $this->encoder_id ) )
315 ) {
316 wp_send_json( array('error' => 'Bad nonce') );
317 }
318
319 $source = sanitize_url( $_POST['source'] );
320 $target = sanitize_text_field( $_POST['target'] );
321
322 // if the extending Encoder supports encryption, add it here
323 if ( isset($_POST['encryption']) ) {
324 $encryption = sanitize_text_field( $_POST['encryption'] );
325 }
326
327 // if the extending Encoder supports a trailer, add it here
328 if ( isset( $_POST['trailer'] ) ) {
329 $trailer = sanitize_text_field( $_POST['trailer'] );
330 }
331
332 $target = $this->util__sanitize_target($target);
333
334 // if we get a proper category link, we prepend its Name (and parent Names) to the target
335 if ( !empty($_POST['category_id']) ) {
336 $this->create_encoding_categories();
337
338 if ( $folder = $this->util__category_id_to_folder( absint( $_POST['category_id'] ) ) ) {
339 $target = $folder.'/'.$target;
340 }
341 }
342
343 if( isset( $_POST['id_video'] ) ) {
344 $id_video = intval( $_POST['id_video'] );
345 }
346
347 // check for a valid source URL
348 if ( empty( $_POST['no_source_verify'] ) && !preg_match('~^(https?|s?ftp)://~', $source) ) {
349 $error = 'Your source location is not a proper URL!';
350 if ( defined('DOING_AJAX') ) {
351 wp_send_json( array('error' => $error) );
352 } else {
353 return $error;
354 }
355 }
356
357 // if the same target name already exists and we've not asked to rename it automatically,
358 // return an error
359 if ( empty( $_POST['rename_if_exists'] ) && empty( $_POST['ignore_duplicates'] ) ) {
360 if ( $wpdb->get_var( $wpdb->prepare( "SELECT count(id) FROM `{$wpdb->prefix}fv_player_encoding_jobs` WHERE target = %s AND status != 'error' AND type = %s", $target, $this->encoder_id ) ) ) {
361 $error = 'Target stream already exists, please try with different target name.';
362 if ( defined( 'DOING_AJAX' ) ) {
363 wp_send_json( array( 'error' => $error ) );
364 } else {
365 return $error;
366 }
367 }
368 } else if ( empty( $_POST['ignore_duplicates'] ) ) {
369 $original_target = $target;
370 $rename_suffix_counter = 1;
371 while ( $wpdb->get_var( $wpdb->prepare( "SELECT count(id) FROM `{$wpdb->prefix}fv_player_encoding_jobs` WHERE target = %s AND status != 'error' AND type = %s", $target, $this->encoder_id ) ) ) {
372 $rename_suffix_counter++;
373 $target = $original_target . '_' . $rename_suffix_counter;
374 }
375 }
376
377 // verify the currently used endpoint supported by the extending Encoder,
378 // such as (S)FTP or S3 credentials
379 $endpoint_verify = $this->verify_active_endpoint( $target );
380 if ( $endpoint_verify !== true ) {
381 return $endpoint_verify;
382 }
383
384 // prepare an encoding job to submit to the extending Encoder
385 $job = array(
386 'source' => $source,
387 'target' => $target,
388 );
389
390 // encryption support
391 if ( isset( $encryption ) ) {
392 $job['encryption'] = $encryption;
393 }
394
395 // support for trailers
396 if ( isset( $trailer ) ) {
397 $job['trailer'] = $trailer;
398 }
399
400 if( isset( $id_video ) ) {
401 $job['id_video'] = $id_video;
402 }
403
404 // create a new job
405 $id = $this->job_create( $job );
406 $show = array( $id );
407
408 // submit the job to the Encoder service
409 $result = $this->job_submit($id);
410
411 do_action( 'fv_player_encoder_job_submit', $id, $job, $result );
412
413 $response = array( 'id' => $id, 'result' => $result );
414
415 if ( ! empty( $_POST['create_player'] ) ) {
416 global $FV_Player_Db;
417 $player_id = $FV_Player_Db->import_player_data( false, false, array(
418 'videos' => array(
419 array(
420 'src' => 'coconut_processing_' . $id,
421 'meta' => array(
422 array(
423 'meta_key' => 'encoding_job_id',
424 'meta_value' => $id,
425 ),
426 )
427 )
428 )
429 ) );
430 $response['player_id'] = $player_id;
431 }
432
433 if( defined('DOING_AJAX') ) {
434 if ( $this->use_wp_list_table && function_exists( 'convert_to_screen' ) ) {
435 $this->include_listing_lib();
436
437 ob_start();
438 $jobs_table = new FV_Player_Encoder_List_Table( array( 'encoder_id' => $this->encoder_id, 'table_name' => $this->table_name ) );
439 $jobs_table->prepare_items($show);
440 $jobs_table->display();
441 $html = ob_get_clean();
442
443 $response['html'] = $html;
444 }
445
446 wp_send_json( $response );
447
448 } else {
449 return $id;
450 }
451 }
452
453 /**
454 * Includes the browser PHP backend file for the extending encoder class.
455 */
456 function init_browser() {
457 // it should not show when picking the media file in dashboard
458 //if( empty( $_GET['page'] ) || strcmp( $_GET['page'], $this->encoder_wp_url_slug ) != 0 ) {
459 if( !empty( $this->browser_inc_file ) ) {
460 include_once( $this->browser_inc_file );
461 }
462 //}
463 }
464
465 /**
466 * Returns an array with all updated jobs' HTML that can be used on admin pages
467 * to refresh jobs table data during the WP heartbeat.
468 *
469 * @param $ids array An array of all job IDs to get HTML output for.
470 *
471 * @return array Returns an array with all updated jobs' HTML that can be used on admin pages
472 * to refresh jobs table data during the WP heartbeat.
473 */
474 function get_updated_rows( $ids ) {
475 $rows = array();
476
477 if( count($ids) > 0 ) {
478 $this->include_listing_lib();
479 // get html for processed rows
480 foreach($ids as $id ) {
481 ob_start();
482 $jobs_table = new FV_Player_Encoder_List_Table( array( 'encoder_id' => $this->encoder_id, 'table_name' => $this->table_name ) );
483 $jobs_table->prepare_items( array($id) );
484 $jobs_table->display();
485 $html = ob_get_clean();
486 preg_match( '/<tbody[\s\S]*?(<tr>[\s\S]*?<\/tr>)[\s\S]*?<\/tbody>/', $html, $matches ); // match row
487
488 $rows[$id] = $matches[1];
489 }
490 }
491
492 return( $rows );
493 }
494
495 /**
496 * Checks pending encoder jobs for status change and update the src
497 * of this file everywhere it's used in players.
498 *
499 * @param false $all If true, all records are retrieved, otherwise only records for the last 30 seconds are selected.
500 *
501 * @return array Returns an array of all IDs that were in processing status and checked for status change.
502 */
503 function jobs_check( $all = false ) {
504 global $wpdb;
505
506 $ids = array();
507 if( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $this->table_name ) ) != $this->table_name ) {
508 return $ids;
509 }
510
511 if ( $all ) {
512 $pending_jobs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}fv_player_encoding_jobs` WHERE type = %s AND status = 'processing'", $this->encoder_id ) );
513
514 } else {
515 $pending_jobs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}fv_player_encoding_jobs` WHERE type = %s AND status = 'processing' AND date_checked < DATE_SUB( UTC_TIMESTAMP(), INTERVAL 30 SECOND )", $this->encoder_id ) );
516 }
517
518 foreach( $pending_jobs AS $pending_job ) {
519 $ids[] = $pending_job->id;
520
521 $check_result = $this->job_check( $pending_job );
522
523 // if this job was completed, update SRC of all players where its temporary placeholder is used
524 if ( $check_result['status'] == 'completed' ) {
525 $this->update_temporary_job_src( $check_result, $pending_job->id );
526 }
527 }
528
529 return $ids;
530 }
531
532 /**
533 * Updates src of all videos where the temporary "encoder_processing_" placeholder was used
534 * for the video given either by the $check_result parameter or the one currently displayed on page.
535 *
536 * @param array $check_result If set, this will be a previous job check result from this encoder.
537 * @param int $job_id If set, this will be a previous job ID for which the $check_result check was made.
538 *
539 * @return array|null Returns job check value which will be either the same as the given $check_result
540 * or a new, real $check_result after a job check.
541 */
542 private function update_temporary_job_src( $check_result = null, $job_id = null ) {
543 global $FV_Player_Db, $fv_fp;
544
545 if ( $check_result ) {
546 $check = $check_result;
547 } else if ( $fv_fp->current_video() ) {
548 if ( !$job_id ) {
549 $check = $this->job_check( (int) substr( $fv_fp->current_video()->getSrc(), strlen( $this->encoder_id . '_processing_' ) ) );
550 } else {
551 $check = $this->job_check( (int) $job_id );
552 }
553
554 } else if ( $job_id ) {
555 $check = $this->job_check( absint( $job_id ) );
556
557 } else {
558 user_error('Could not retrieve JOB check for encoder ' . $this->encoder_name . ', job ID: ' . $job_id . ', defaulted back to input value: ' . print_r( $check_result, true ), E_USER_WARNING );
559 return $check_result;
560 }
561
562 $temporary_src = $this->encoder_id . '_processing_' . (int) $job_id;
563
564 if ( strcmp( $check['status'], 'completed' ) == 0 && ! empty( $check['output'] ) ) {
565 $job_output = $check['output'];
566
567 // if we don't have current_video then we're on the players listing page, so we need to find and update
568 // all players where our temporary "encoder_processing_" placeholder is used
569 if ( !$fv_fp->current_video() ) {
570 $videos = $FV_Player_Db->query_videos( array(
571 'fields_to_search' => array('src'),
572 'search_string' => $temporary_src,
573 'like' => false,
574 'and_or' => 'OR'
575 )
576 );
577
578 if(!empty($videos)) {
579 foreach ( $videos as $video ) {
580 $res = $this->update_temporary_job_video( $video, $temporary_src, $job_output );
581
582 if ( $res ) {
583 // purge HTML caches for all posts where players containing this video are present
584 $players = $fv_fp->get_players_by_video_ids( $video->getId() );
585 foreach ( $players as $player ) {
586 if ( $posts = $player->getMetaValue( 'post_id' ) ) {
587 foreach ( $posts as $post_id ) {
588 wp_update_post( array( 'ID' => $post_id ) );
589 }
590 }
591 }
592 }
593 }
594 }
595
596 // If not, update the video with the job output if $fv_fp->current_video()->getSrc() ends with $temporary_src
597 } else {
598 $res = $this->update_temporary_job_video( $fv_fp->current_video(), $temporary_src, $job_output );
599
600 if ( $res ) {
601 // purge HTML caches for all posts where this player is present
602 if ( $posts = $fv_fp->current_player()->getMetaValue( 'post_id' ) ) {
603 foreach ( $posts as $post_id ) {
604 wp_update_post( array( 'ID' => $post_id ) );
605 }
606 }
607 }
608 }
609 }
610
611 return $check;
612 }
613
614 function update_temporary_job_video( $video, $temporary_src, $job_output ) {
615
616 /**
617 * Ensure $video->getSrc() ends with $temporary_src
618 * This ensures we match coconut_processing_1 in http://coconut_processing_1,
619 * but not in http://coconut_processing_10
620 */
621 if ( substr( $video->getSrc(), -strlen( $temporary_src ) ) !== $temporary_src ) {
622 return false;
623 }
624
625 // video processed, replace its SRC
626 if ( ! empty( $job_output->src[0] ) ) {
627 $video->set( 'src', $job_output->src[0] );
628 }
629
630 // also replace its thumbnail / splash
631 if ( ! empty( $job_output->thumbnail_large ) ) {
632 $video->set( 'splash', $job_output->thumbnail_large );
633
634 } else if ( ! empty( $job_output->thumbnail ) ) {
635 $video->set( 'splash', $job_output->thumbnail );
636 } else if ( ! empty( $job_output->splash ) ) {
637 $video->set( 'splash', $job_output->splash );
638 }
639
640 if ( ! empty( $job_output->hlskey ) ) {
641 $video->updateMetaValue( 'hls_hlskey', $job_output->hlskey );
642 }
643
644 // also set its timeline preview, if received
645 if ( ! empty( $job_output->timeline_previews ) ) {
646 $video->updateMetaValue( 'timeline_previews', $job_output->timeline_previews );
647 }
648
649 // save changes for this video
650 return $video->save();
651 }
652
653 /**
654 * Create the job database entry.
655 *
656 * @param array $args Job configuration
657 * $args = array(
658 * 'source' (string) Source file URL
659 * 'target' (string) Target video folder
660 * 'encryption' (bool) (optional, encoder-features-dependent) Encrypt the HLS stream or not
661 * 'trailer' (bool) (optional, encoder-features-dependent) Should it be a small part of video only
662 *
663 * @global object $wpdb WordPress database object
664 *
665 * @return ID Job ID
666 */
667 public function job_create( $args ) {
668 global $wpdb, $fv_fp;
669
670 $args = wp_parse_args( $args, array(
671 'encryption' => false,
672 'trailer' => false,
673 'id_video' => false
674 ) );
675
676 $video_ids = explode( ',', strval($args['id_video']) );
677
678 // first we instert the table row with basic data and remember the row ID
679 $wpdb->insert( $this->table_name, array(
680 'date_created' => gmdate("Y-m-d H:i:s"),
681 'id_video' => $args['id_video'],
682 'source' => $args['source'],
683 'target' => $args['target'],
684 'type' => $this->encoder_id,
685 'mime' => $fv_fp->get_mime_type( $args['source'] ),
686 'status' => 'created',
687 'output' => $this->prepare_job_output_column_value(),
688 'args' => '',
689 'author' => get_current_user_id(),
690 'id_video' => $video_ids[0]
691 ), array(
692 '%s',
693 '%d',
694 '%s',
695 '%s',
696 '%s',
697 '%s',
698 '%s',
699 '%s',
700 '%s',
701 '%d',
702 '%d'
703 ));
704
705 $job_id = $wpdb->insert_id;
706 if( !$job_id ) {
707 wp_send_json( array('error' => 'Database error') );
708 return;
709 }
710
711 // we apply extra sanitizaion as some encoders (such as Coconut) use bare text format for their configs
712 $source = $this->util__escape_source($args['source']);
713
714 // we apply the URL signatures/tokens
715 add_filter( 'fv_player_secure_link_timeout', array( $this, 'job_create_expiration' ) );
716 $source = apply_filters( 'fv_flowplayer_video_src', $source, array( 'dynamic' => true ) );
717
718 // once we have the row ID, we generate the configuration
719 $conf_array = array(
720 'source' => $source,
721 'target' => $args['target'],
722 'job_id' => $job_id,
723 'video_id' => $args['id_video'],
724 );
725
726 if ( isset( $args['encryption'] ) ) {
727 $conf_array['encryption'] = $args['encryption'];
728 }
729
730 if ( isset( $args['trailer'] ) ) {
731 $conf_array['trailer'] = $args['trailer'];
732 }
733
734 $conf = $this->get_conf( $conf_array );
735
736 // store the final configuration
737 $wpdb->update( $this->table_name, array(
738 'args' => wp_json_encode( $conf )
739 ), array(
740 'id' => $job_id
741 ), array(
742 '%s'
743 ), array(
744 '%d'
745 ) );
746
747 return $job_id;
748 }
749
750 /**
751 * Adds filtering options for the jobs listing page.
752 */
753 function screen_options() {
754 $screen = get_current_screen();
755 if ( !is_object($screen) || $screen->id != $this->admin_page ) return;
756
757 $args = array(
758 'label' => __('Jobs per page', 'pippin'),
759 'default' => 25,
760 'option' => 'fv_player_' . $this->encoder_id . '_per_page'
761 );
762
763 add_screen_option( 'per_page', $args );
764 }
765
766 /**
767 * Sets the per-page option value for job listing page filter.
768 *
769 * @param $status string Unused.
770 * @param $option string Name of the option we're checking for.
771 * @param $value string Value of the option we're checking for.
772 *
773 * @return string|void
774 */
775 function set_screen_option($status, $option, $value) {
776 if ( 'fv_player_' . $this->encoder_id . '_per_page' == $option ) return $value;
777 }
778
779 /**
780 * Adds the title and tabs for the jobs listing encoder page in Admin.
781 */
782 function tools_panel() {
783 if ( !$this->is_configured() ) {
784 $this->tools_panel_settings();
785 return;
786 }
787
788 ?>
789 <div class="wrap">
790 <h1 class="wp-heading-inline">FV Player <?php echo esc_html( $this->encoder_name ); ?> Video Encoding Jobs</h1>
791 <h2 class="nav-tab-wrapper">
792 <a href="<?php echo add_query_arg( 'page', $this->encoder_wp_url_slug, admin_url('admin.php') ) ?>" class="nav-tab<?php if( $this->tools_panel_is('jobs') ) echo ' nav-tab-active'; ?>">Jobs</a>
793 <a href="<?php echo add_query_arg( array('page' => $this->encoder_wp_url_slug ,'panel' => 'settings'), admin_url('admin.php') ) ?>" class="nav-tab<?php if( $this->tools_panel_is('settings') ) echo ' nav-tab-active'; ?>">Settings</a>
794 </h2>
795 <?php
796 if( $this->tools_panel_is('settings') ) {
797 $this->tools_panel_settings();
798 } else {
799 $this->tools_panel_jobs();
800 }
801 ?>
802 </div>
803 <?php
804 }
805
806 /**
807 * Checks what kind of tab we have active in the jobs listing page in Admin.
808 *
809 * @param boolean $kind The kind of tab we're comparing currently displayed tab with.
810 *
811 * @return bool Returns true if the tab we're looking for is active, false otherwise.
812 */
813 function tools_panel_is( $kind = false ) {
814 $panel = !empty( $_GET['panel'] ) ? sanitize_key( $_GET['panel'] ) : 'jobs';
815 return strcmp( $panel, $kind ) == 0;
816 }
817
818 /**
819 * Includes JS for the extending encoder class.
820 *
821 * @param $page Auto-filled by WP by the page slug at which we're looking.
822 */
823 public function admin_enqueue_scripts( $page ) {
824 if( $page == 'post.php' || $page == 'post-new.php' || $page == 'toplevel_page_fv_player' || $page == 'settings_page_fvplayer' || $page == 'fv-player_page_' . $this->encoder_wp_url_slug ) {
825 $file = $this->locate_script('admin.js');
826 if( $file ) {
827 $handle = 'fv_player_' . $this->encoder_id . '_admin';
828 wp_enqueue_script( $handle, plugins_url( $file, $this->getFILE() ), array('jquery'), filemtime( dirname( $this->getFILE() ) . $file), true );
829 wp_localize_script( $handle, $this->encoder_id . '_pending_jobs', $this->jobs_check(true) );
830 }
831 }
832 }
833
834 /**
835 * Adds a hidden encoding job ID field into the editor.
836 */
837 function shortcode_editor_item() {
838 // TODO: The field has to start with fv_wp_flowplayer_field_ which is not easy to keep in mind!
839 ?>
840 <input type="hidden" id="fv_wp_flowplayer_field_encoding_job_id" name="fv_wp_flowplayer_field_encoding_job_id" />
841 <?php
842 }
843
844 /**
845 * Converts 'Tom & Jerry - "The Best" show' to Tom-Jerry-The-Best-show to
846 * ensure safe directory names
847 *
848 * @param string $filename The filename of the source video
849 *
850 * @return string Sanitized file URL - name of the resulting folder for video
851 */
852 function util__escape_filename( $filename ) {
853 // allow only safe characters
854 $filename = preg_replace('/[^A-Za-z0-9\-]/m', '-', $filename);
855 $filename = preg_replace('/-{2,}/m', '-', $filename);
856 // remove - at start or beginning
857 $filename = preg_replace('/^-|-$/m', '', $filename);
858 return $filename;
859 }
860
861 /**
862 * Without this Coconut wouldn't accept file URLs with symbols like ' ' or
863 * & in it
864 *
865 * @param string $url Source video file URL
866 *
867 * @return string Sanitized file URL
868 */
869 function util__escape_source( $url ) {
870 $url_components = wp_parse_url($url);
871 $old_path = $url_components['path'];
872
873 $url_components['path'] = str_replace( array('%20','+'), ' ', $url_components['path']);
874
875 $url_components['path'] = rawurlencode($url_components['path']);
876 $url_components['path'] = str_replace('%2F', '/', $url_components['path']);
877 $url_components['path'] = str_replace('%2B', '+', $url_components['path']);
878
879 $url = str_replace($old_path, $url_components['path'], $url);
880 return $url;
881 }
882
883 /**
884 * Convert fv_player_encoding_category ID to a nice folder name.
885 * If you have:
886 * - Documentaries
887 * -- Nature
888 * --- Wildlife & Adventure
889 *
890 * you get: Documentaries/Nature/Wildlife-Adventure
891 *
892 * @param string $url Source video file URL
893 *
894 * @return string Sanitized file URL
895 */
896 function util__category_id_to_folder( $category_id ) {
897 $folder = false;
898
899 $category = get_term($category_id);
900 if( !is_wp_error($category) ) {
901 $hierarchy = array( $this->util__escape_filename($category->name) );
902 $ancestors = get_ancestors( $category->term_id, 'fv_player_encoding_category', 'taxonomy' );
903 foreach( (array)$ancestors as $ancestor ) {
904 $ancestor_term = get_term($ancestor, 'fv_player_encoding_category');
905 $hierarchy[] = $this->util__escape_filename($ancestor_term->name);
906 }
907 $hierarchy = array_reverse($hierarchy);
908 $folder = implode('/', $hierarchy);
909 }
910
911 return $folder;
912 }
913
914 /**
915 * Get sanitized file path. For example https://cdn.site.com/lessons/music/composing/lesson-1.mp4 gives you /lessons/music/composing/lesson-1
916 *
917 * @param $string Filename or URL
918 * @return string
919 */
920 function util__sanitize_target( $target ) {
921
922 $target = trim($target);
923
924 // take path only if it's full URL
925 $parsed = wp_parse_url($target);
926
927 if( !empty($parsed['scheme']) ) $target = str_replace($parsed['scheme'].'://', '', $parsed);
928 if( !empty($parsed['hostname']) ) $target = str_replace($parsed['hostname'], '', $parsed);
929
930 $target = preg_replace( '~/$~', '', $target ); // remove trailing slash
931
932 // sanitize filename
933 $target = explode('/', $target);
934
935 // deal with %20 encoding of spaces
936 $target = array_map( 'urldecode', $target );
937
938 $filename = $target[ count($target) - 1 ];
939
940 // remove file extension
941 if( strrpos( $filename, ".") ) {
942 $filename = substr( $filename, 0, strrpos( $filename, "."));
943 }
944
945 $filename = $this->util__escape_filename($filename);
946
947 // we're done
948 $target[ count($target) - 1 ] = $filename;
949 $target = join('/', $target);
950
951 return $target;
952 }
953
954 /**
955 * Sends an e-mail about changes in the encoding job.
956 *
957 * @param $id int int ID of the job to send this e-mail about.
958 * @param $author_id int ID of the author of the encoding job.
959 * @param $status string Status of the processed encoding job.
960 * @param $target string The actual target for the processed encoding job.
961 * @param $result string Text representation of the result, used to send any error messages along with the e-mail.
962 */
963 function send_email( $id, $author_id, $status, $target, $result ) {
964 $user = get_userdata( $author_id );
965 $to = $user->user_email;
966 $headers = array('Content-Type: text/plain; charset=UTF-8');
967
968 $subject = "[". get_bloginfo( 'name' ) . "] FV Player {$this->encoder_name}: Job #" . $id . " " . $target . " " . $status ;
969
970 $body = "Hello " . $user->display_name . ",\r\n";
971 $body .= "Your encoding job #" . $id . " " . $target . " has ";
972
973 if( $status == 'completed' ) {
974 $body .= "successfully finished.\r\n";
975 } else {
976 $body .= "run into some problems.\r\n";
977 $body .= $result."\r\n";
978 }
979
980 if ( user_can( $author_id, 'manage_options' ) ) {
981 $body .= "\r\nManage video encoding jobs <a href='". admin_url( 'admin.php?page=' . $this->encoder_wp_url_slug ) ."'>here</a>";
982 }
983
984 wp_mail( $to, $subject, $body, $headers );
985 }
986
987 /**
988 * Updates DB table definition for the extending plugin.
989 * Used when a version change of the extending plugin is detected,
990 * as well as displaying jobs listing page.
991 */
992 public function plugin_update_database() {
993 global $wpdb;
994
995 $sql = "CREATE TABLE ". $this->table_name ." (
996 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
997 id_video bigint(20) unsigned NOT NULL,
998 job_id varchar(45) NOT NULL,
999 date_created datetime NOT NULL,
1000 date_checked datetime NOT NULL,
1001 source varchar(1024) NOT NULL,
1002 target varchar(1024) NOT NULL,
1003 type varchar(64) NOT NULL,
1004 status varchar(64) NOT NULL,
1005 progress varchar(64),
1006 error varchar(1024),
1007 mime varchar(64),
1008 args TEXT,
1009 result TEXT,
1010 output TEXT,
1011 video_data TEXT,
1012 author bigint(20) unsigned NOT NULL default '0',
1013 fv_player_encoding_category_id bigint(20) unsigned DEFAULT NULL,
1014 PRIMARY KEY (id),
1015 KEY source (source(191)),
1016 KEY type (type),
1017 KEY status (status),
1018 KEY job_id (job_id(15))
1019 )" . $wpdb->get_charset_collate() . ";";
1020
1021 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
1022 dbDelta( $sql );
1023 }
1024
1025 /**
1026 * Displays general notices on top in Admin pages.
1027 */
1028 abstract function admin_notices();
1029
1030 /**
1031 * Returns an updated $conf variable with settings for the extending Encoder class.
1032 *
1033 * @param $conf Pre-populated configuration array into which the extending Encoder's class configuration should go.
1034 *
1035 * @return array Returns an updated $conf variable with settings for the extending Encoder class.
1036 */
1037 abstract function default_settings( $conf );
1038
1039 /**
1040 * Verifies the currently used endpoint supported by the extending Encoder, such as (S)FTP or S3 credentials
1041 * and either directly outputs a JSON-formatted error (for AJAX purposes) or returns the error to be processed further.
1042 *
1043 * @return mixed Returns TRUE if the current endpoint is set up properly, an error object/array otherwise.
1044 * If we're running an AJAX request, this method must return a valid JSON-formatted error for that request
1045 * by utilizing the wp_send_json() method in this format: wp_send_json( array('error' => $error) );
1046 */
1047 protected abstract function verify_active_endpoint( $target );
1048
1049 /*
1050 * Creates default Encoder's configuration.
1051 */
1052 abstract function get_conf( $args );
1053
1054 /**
1055 * Determines whether this Encoder has been properly configured.
1056 */
1057 abstract function is_configured();
1058
1059 /**
1060 * Prepares and returns data to be inserted into the "output" column of this encoder's DB table.
1061 */
1062 abstract protected function prepare_job_output_column_value();
1063
1064 /**
1065 * Retrieves new encoding job expiration time, used in URL signatures / tokens.
1066 *
1067 * @param $ttl An optional TTL parameter.
1068 *
1069 * @return int Returns the duration in seconds for which this job is valid.
1070 */
1071 abstract public function job_create_expiration( $ttl );
1072
1073 /**
1074 * Update job status
1075 *
1076 * @param object|int $pending_job Table row from encoder's table or its job ID
1077 *
1078 * @global object $wpdb WordPress database object
1079 * @global object $fv_fp FV Player
1080 *
1081 * @return array
1082 * array(
1083 * 'result' object Job info from the Encoder
1084 * 'status' string Valid values are: "processing", "completed", "error"
1085 * 'output' object URLs for all processed resources (such as video qualities, thumbnails etc.)
1086 * )
1087 */
1088 abstract protected function job_check( $pending_job );
1089
1090 /**
1091 * Submits the job to the Encoder service and stores the result in a table.
1092 *
1093 * @param int $job_id Job ID
1094
1095 * @global object $wpdb WordPress database object
1096 * @global object $fv_fp FV Player instance to load options with
1097 *
1098 * @return bool Result
1099 */
1100 abstract function job_submit( $id );
1101
1102 /**
1103 * Displays the jobs listing page contents.
1104 */
1105 abstract function tools_panel_jobs();
1106
1107 /**
1108 * Displays the Encoder's settings page contents.
1109 */
1110 abstract function tools_panel_settings();
1111
1112 /**
1113 * Must return __FILE__ from the extending class.
1114 * Used to determine plugin path for registering JS and CSS.
1115 */
1116 abstract function getFILE();
1117
1118 /**
1119 * Send out an e-mail notification of an encoding job change.
1120 * To be used when a WebHook is fired from the Encoding service.
1121 */
1122 abstract function email_notification();
1123
1124 }
1125