PluginProbe
FV Player 8 / trunk
FV Player 8 vtrunk
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 trunk, at models/video-encoder/video-encoder.php

1,123 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 $trailer = ! empty( $_POST['trailer'] );
329
330 $target = $this->util__sanitize_target($target);
331
332 // if we get a proper category link, we prepend its Name (and parent Names) to the target
333 if ( !empty($_POST['category_id']) ) {
334 $this->create_encoding_categories();
335
336 if ( $folder = $this->util__category_id_to_folder( absint( $_POST['category_id'] ) ) ) {
337 $target = $folder.'/'.$target;
338 }
339 }
340
341 if( isset( $_POST['id_video'] ) ) {
342 $id_video = intval( $_POST['id_video'] );
343 }
344
345 // check for a valid source URL
346 if ( empty( $_POST['no_source_verify'] ) && !preg_match('~^(https?|s?ftp)://~', $source) ) {
347 $error = 'Your source location is not a proper URL!';
348 if ( defined('DOING_AJAX') ) {
349 wp_send_json( array('error' => $error) );
350 } else {
351 return $error;
352 }
353 }
354
355 // if the same target name already exists and we've not asked to rename it automatically,
356 // return an error
357 if ( empty( $_POST['rename_if_exists'] ) && empty( $_POST['ignore_duplicates'] ) ) {
358 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 ) ) ) {
359 $error = 'Target stream already exists, please try with different target name.';
360 if ( defined( 'DOING_AJAX' ) ) {
361 wp_send_json( array( 'error' => $error ) );
362 } else {
363 return $error;
364 }
365 }
366 } else if ( empty( $_POST['ignore_duplicates'] ) ) {
367 $original_target = $target;
368 $rename_suffix_counter = 1;
369 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 ) ) ) {
370 $rename_suffix_counter++;
371 $target = $original_target . '_' . $rename_suffix_counter;
372 }
373 }
374
375 // verify the currently used endpoint supported by the extending Encoder,
376 // such as (S)FTP or S3 credentials
377 $endpoint_verify = $this->verify_active_endpoint( $target );
378 if ( $endpoint_verify !== true ) {
379 return $endpoint_verify;
380 }
381
382 // prepare an encoding job to submit to the extending Encoder
383 $job = array(
384 'source' => $source,
385 'target' => $target,
386 );
387
388 // encryption support
389 if ( isset( $encryption ) ) {
390 $job['encryption'] = $encryption;
391 }
392
393 // support for trailers
394 $job['trailer'] = $trailer;
395
396 if( isset( $id_video ) ) {
397 $job['id_video'] = $id_video;
398 }
399
400 // create a new job
401 $id = $this->job_create( $job );
402 $show = array( $id );
403
404 // submit the job to the Encoder service
405 $result = $this->job_submit($id);
406
407 do_action( 'fv_player_encoder_job_submit', $id, $job, $result );
408
409 $response = array( 'id' => $id, 'result' => $result );
410
411 if ( ! empty( $_POST['create_player'] ) ) {
412 global $FV_Player_Db;
413 $player_id = $FV_Player_Db->import_player_data( false, false, array(
414 'videos' => array(
415 array(
416 'src' => 'coconut_processing_' . $id,
417 'meta' => array(
418 array(
419 'meta_key' => 'encoding_job_id',
420 'meta_value' => $id,
421 ),
422 )
423 )
424 )
425 ) );
426 $response['player_id'] = $player_id;
427 }
428
429 if( defined('DOING_AJAX') ) {
430 if ( $this->use_wp_list_table && function_exists( 'convert_to_screen' ) ) {
431 $this->include_listing_lib();
432
433 ob_start();
434 $jobs_table = new FV_Player_Encoder_List_Table( array( 'encoder_id' => $this->encoder_id, 'table_name' => $this->table_name ) );
435 $jobs_table->prepare_items($show);
436 $jobs_table->display();
437 $html = ob_get_clean();
438
439 $response['html'] = $html;
440 }
441
442 wp_send_json( $response );
443
444 } else {
445 return $id;
446 }
447 }
448
449 /**
450 * Includes the browser PHP backend file for the extending encoder class.
451 */
452 function init_browser() {
453 // it should not show when picking the media file in dashboard
454 //if( empty( $_GET['page'] ) || strcmp( $_GET['page'], $this->encoder_wp_url_slug ) != 0 ) {
455 if( !empty( $this->browser_inc_file ) ) {
456 include_once( $this->browser_inc_file );
457 }
458 //}
459 }
460
461 /**
462 * Returns an array with all updated jobs' HTML that can be used on admin pages
463 * to refresh jobs table data during the WP heartbeat.
464 *
465 * @param $ids array An array of all job IDs to get HTML output for.
466 *
467 * @return array Returns an array with all updated jobs' HTML that can be used on admin pages
468 * to refresh jobs table data during the WP heartbeat.
469 */
470 function get_updated_rows( $ids ) {
471 $rows = array();
472
473 if( count($ids) > 0 ) {
474 $this->include_listing_lib();
475 // get html for processed rows
476 foreach($ids as $id ) {
477 ob_start();
478 $jobs_table = new FV_Player_Encoder_List_Table( array( 'encoder_id' => $this->encoder_id, 'table_name' => $this->table_name ) );
479 $jobs_table->prepare_items( array($id) );
480 $jobs_table->display();
481 $html = ob_get_clean();
482 preg_match( '/<tbody[\s\S]*?(<tr>[\s\S]*?<\/tr>)[\s\S]*?<\/tbody>/', $html, $matches ); // match row
483
484 $rows[$id] = $matches[1];
485 }
486 }
487
488 return( $rows );
489 }
490
491 /**
492 * Checks pending encoder jobs for status change and update the src
493 * of this file everywhere it's used in players.
494 *
495 * @param false $all If true, all records are retrieved, otherwise only records for the last 30 seconds are selected.
496 *
497 * @return array Returns an array of all IDs that were in processing status and checked for status change.
498 */
499 function jobs_check( $all = false ) {
500 global $wpdb;
501
502 $ids = array();
503 if( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $this->table_name ) ) != $this->table_name ) {
504 return $ids;
505 }
506
507 if ( $all ) {
508 $pending_jobs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}fv_player_encoding_jobs` WHERE type = %s AND status = 'processing'", $this->encoder_id ) );
509
510 } else {
511 $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 ) );
512 }
513
514 foreach( $pending_jobs AS $pending_job ) {
515 $ids[] = $pending_job->id;
516
517 $check_result = $this->job_check( $pending_job );
518
519 // if this job was completed, update SRC of all players where its temporary placeholder is used
520 if ( ! empty( $check_result['status'] ) && $check_result['status'] == 'completed' ) {
521 $this->update_temporary_job_src( $check_result, $pending_job->id );
522 }
523 }
524
525 return $ids;
526 }
527
528 /**
529 * Updates src of all videos where the temporary "encoder_processing_" placeholder was used
530 * for the video given either by the $check_result parameter or the one currently displayed on page.
531 *
532 * @param array $check_result If set, this will be a previous job check result from this encoder.
533 * @param int $job_id If set, this will be a previous job ID for which the $check_result check was made.
534 *
535 * @return array|null Returns job check value which will be either the same as the given $check_result
536 * or a new, real $check_result after a job check.
537 */
538 private function update_temporary_job_src( $check_result = null, $job_id = null ) {
539 global $FV_Player_Db, $fv_fp;
540
541 if ( $check_result ) {
542 $check = $check_result;
543 } else if ( $fv_fp->current_video() ) {
544 if ( !$job_id ) {
545 $check = $this->job_check( (int) substr( $fv_fp->current_video()->getSrc(), strlen( $this->encoder_id . '_processing_' ) ) );
546 } else {
547 $check = $this->job_check( (int) $job_id );
548 }
549
550 } else if ( $job_id ) {
551 $check = $this->job_check( absint( $job_id ) );
552
553 } else {
554 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 );
555 return $check_result;
556 }
557
558 $temporary_src = $this->encoder_id . '_processing_' . (int) $job_id;
559
560 if ( strcmp( $check['status'], 'completed' ) == 0 && ! empty( $check['output'] ) ) {
561 $job_output = $check['output'];
562
563 // if we don't have current_video then we're on the players listing page, so we need to find and update
564 // all players where our temporary "encoder_processing_" placeholder is used
565 if ( !$fv_fp->current_video() ) {
566 $videos = $FV_Player_Db->query_videos( array(
567 'fields_to_search' => array('src'),
568 'search_string' => $temporary_src,
569 'like' => false,
570 'and_or' => 'OR'
571 )
572 );
573
574 if(!empty($videos)) {
575 foreach ( $videos as $video ) {
576 $res = $this->update_temporary_job_video( $video, $temporary_src, $job_output );
577
578 if ( $res ) {
579 // purge HTML caches for all posts where players containing this video are present
580 $players = $fv_fp->get_players_by_video_ids( $video->getId() );
581 foreach ( $players as $player ) {
582 if ( $posts = $player->getMetaValue( 'post_id' ) ) {
583 foreach ( $posts as $post_id ) {
584 wp_update_post( array( 'ID' => $post_id ) );
585 }
586 }
587 }
588 }
589 }
590 }
591
592 // If not, update the video with the job output if $fv_fp->current_video()->getSrc() ends with $temporary_src
593 } else {
594 $res = $this->update_temporary_job_video( $fv_fp->current_video(), $temporary_src, $job_output );
595
596 if ( $res ) {
597 // purge HTML caches for all posts where this player is present
598 if ( $posts = $fv_fp->current_player()->getMetaValue( 'post_id' ) ) {
599 foreach ( $posts as $post_id ) {
600 wp_update_post( array( 'ID' => $post_id ) );
601 }
602 }
603 }
604 }
605 }
606
607 return $check;
608 }
609
610 function update_temporary_job_video( $video, $temporary_src, $job_output ) {
611
612 /**
613 * Ensure $video->getSrc() ends with $temporary_src
614 * This ensures we match coconut_processing_1 in http://coconut_processing_1,
615 * but not in http://coconut_processing_10
616 */
617 if ( substr( $video->getSrc(), -strlen( $temporary_src ) ) !== $temporary_src ) {
618 return false;
619 }
620
621 // video processed, replace its SRC
622 if ( ! empty( $job_output->src[0] ) ) {
623 $video->set( 'src', $job_output->src[0] );
624 }
625
626 // also replace its thumbnail / splash
627 if ( ! empty( $job_output->thumbnail_large ) ) {
628 $video->set( 'splash', $job_output->thumbnail_large );
629
630 } else if ( ! empty( $job_output->thumbnail ) ) {
631 $video->set( 'splash', $job_output->thumbnail );
632 } else if ( ! empty( $job_output->splash ) ) {
633 $video->set( 'splash', $job_output->splash );
634 }
635
636 if ( ! empty( $job_output->hlskey ) ) {
637 $video->updateMetaValue( 'hls_hlskey', $job_output->hlskey );
638 }
639
640 // also set its timeline preview, if received
641 if ( ! empty( $job_output->timeline_previews ) ) {
642 $video->updateMetaValue( 'timeline_previews', $job_output->timeline_previews );
643 }
644
645 // save changes for this video
646 return $video->save();
647 }
648
649 /**
650 * Create the job database entry.
651 *
652 * @param array $args Job configuration
653 * $args = array(
654 * 'source' (string) Source file URL
655 * 'target' (string) Target video folder
656 * 'encryption' (bool) (optional, encoder-features-dependent) Encrypt the HLS stream or not
657 * 'trailer' (bool) (optional, encoder-features-dependent) Should it be a small part of video only
658 *
659 * @global object $wpdb WordPress database object
660 *
661 * @return ID Job ID
662 */
663 public function job_create( $args ) {
664 global $wpdb, $fv_fp;
665
666 $args = wp_parse_args( $args, array(
667 'encryption' => false,
668 'trailer' => false,
669 'id_video' => false
670 ) );
671
672 $video_ids = explode( ',', strval($args['id_video']) );
673
674 // first we instert the table row with basic data and remember the row ID
675 $wpdb->insert( $this->table_name, array(
676 'date_created' => gmdate("Y-m-d H:i:s"),
677 'id_video' => $args['id_video'],
678 'source' => $args['source'],
679 'target' => $args['target'],
680 'type' => $this->encoder_id,
681 'mime' => $fv_fp->get_mime_type( $args['source'] ),
682 'status' => 'created',
683 'output' => $this->prepare_job_output_column_value(),
684 'args' => '',
685 'author' => get_current_user_id(),
686 'id_video' => $video_ids[0]
687 ), array(
688 '%s',
689 '%d',
690 '%s',
691 '%s',
692 '%s',
693 '%s',
694 '%s',
695 '%s',
696 '%s',
697 '%d',
698 '%d'
699 ));
700
701 $job_id = $wpdb->insert_id;
702 if( !$job_id ) {
703 wp_send_json( array('error' => 'Database error') );
704 return;
705 }
706
707 // we apply extra sanitizaion as some encoders (such as Coconut) use bare text format for their configs
708 $source = $this->util__escape_source($args['source']);
709
710 // we apply the URL signatures/tokens
711 add_filter( 'fv_player_secure_link_timeout', array( $this, 'job_create_expiration' ) );
712 $source = apply_filters( 'fv_flowplayer_video_src', $source, array( 'dynamic' => true ) );
713
714 // once we have the row ID, we generate the configuration
715 $conf_array = array(
716 'source' => $source,
717 'target' => $args['target'],
718 'job_id' => $job_id,
719 'video_id' => $args['id_video'],
720 );
721
722 if ( isset( $args['encryption'] ) ) {
723 $conf_array['encryption'] = $args['encryption'];
724 }
725
726 if ( isset( $args['trailer'] ) ) {
727 $conf_array['trailer'] = $args['trailer'];
728 }
729
730 $conf = $this->get_conf( $conf_array );
731
732 // store the final configuration
733 $wpdb->update( $this->table_name, array(
734 'args' => wp_json_encode( $conf )
735 ), array(
736 'id' => $job_id
737 ), array(
738 '%s'
739 ), array(
740 '%d'
741 ) );
742
743 return $job_id;
744 }
745
746 /**
747 * Adds filtering options for the jobs listing page.
748 */
749 function screen_options() {
750 $screen = get_current_screen();
751 if ( !is_object($screen) || $screen->id != $this->admin_page ) return;
752
753 $args = array(
754 'label' => __('Jobs per page', 'pippin'),
755 'default' => 25,
756 'option' => 'fv_player_' . $this->encoder_id . '_per_page'
757 );
758
759 add_screen_option( 'per_page', $args );
760 }
761
762 /**
763 * Sets the per-page option value for job listing page filter.
764 *
765 * @param $status string Unused.
766 * @param $option string Name of the option we're checking for.
767 * @param $value string Value of the option we're checking for.
768 *
769 * @return string|void
770 */
771 function set_screen_option($status, $option, $value) {
772 if ( 'fv_player_' . $this->encoder_id . '_per_page' == $option ) return $value;
773 }
774
775 /**
776 * Adds the title and tabs for the jobs listing encoder page in Admin.
777 */
778 function tools_panel() {
779 if ( !$this->is_configured() ) {
780 $this->tools_panel_settings();
781 return;
782 }
783
784 ?>
785 <div class="wrap">
786 <h1 class="wp-heading-inline">FV Player <?php echo esc_html( $this->encoder_name ); ?> Video Encoding Jobs</h1>
787 <h2 class="nav-tab-wrapper">
788 <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>
789 <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>
790 </h2>
791 <?php
792 if( $this->tools_panel_is('settings') ) {
793 $this->tools_panel_settings();
794 } else {
795 $this->tools_panel_jobs();
796 }
797 ?>
798 </div>
799 <?php
800 }
801
802 /**
803 * Checks what kind of tab we have active in the jobs listing page in Admin.
804 *
805 * @param boolean $kind The kind of tab we're comparing currently displayed tab with.
806 *
807 * @return bool Returns true if the tab we're looking for is active, false otherwise.
808 */
809 function tools_panel_is( $kind = false ) {
810 $panel = !empty( $_GET['panel'] ) ? sanitize_key( $_GET['panel'] ) : 'jobs';
811 return strcmp( $panel, $kind ) == 0;
812 }
813
814 /**
815 * Includes JS for the extending encoder class.
816 *
817 * @param $page Auto-filled by WP by the page slug at which we're looking.
818 */
819 public function admin_enqueue_scripts( $page ) {
820 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 ) {
821 $file = $this->locate_script('admin.js');
822 if( $file ) {
823 $handle = 'fv_player_' . $this->encoder_id . '_admin';
824 wp_enqueue_script( $handle, plugins_url( $file, $this->getFILE() ), array('jquery'), filemtime( dirname( $this->getFILE() ) . $file), true );
825 wp_localize_script( $handle, $this->encoder_id . '_pending_jobs', $this->jobs_check(true) );
826 }
827 }
828 }
829
830 /**
831 * Adds a hidden encoding job ID field into the editor.
832 */
833 function shortcode_editor_item() {
834 // TODO: The field has to start with fv_wp_flowplayer_field_ which is not easy to keep in mind!
835 ?>
836 <input type="hidden" id="fv_wp_flowplayer_field_encoding_job_id" name="fv_wp_flowplayer_field_encoding_job_id" />
837 <?php
838 }
839
840 /**
841 * Converts 'Tom & Jerry - "The Best" show' to Tom-Jerry-The-Best-show to
842 * ensure safe directory names
843 *
844 * @param string $filename The filename of the source video
845 *
846 * @return string Sanitized file URL - name of the resulting folder for video
847 */
848 function util__escape_filename( $filename ) {
849 // allow only safe characters
850 $filename = preg_replace('/[^A-Za-z0-9\-]/m', '-', $filename);
851 $filename = preg_replace('/-{2,}/m', '-', $filename);
852 // remove - at start or beginning
853 $filename = preg_replace('/^-|-$/m', '', $filename);
854 return $filename;
855 }
856
857 /**
858 * Without this Coconut wouldn't accept file URLs with symbols like ' ' or
859 * & in it
860 *
861 * @param string $url Source video file URL
862 *
863 * @return string Sanitized file URL
864 */
865 function util__escape_source( $url ) {
866 $url_components = wp_parse_url($url);
867 $old_path = $url_components['path'];
868
869 $url_components['path'] = str_replace( array('%20','+'), ' ', $url_components['path']);
870
871 $url_components['path'] = rawurlencode($url_components['path']);
872 $url_components['path'] = str_replace('%2F', '/', $url_components['path']);
873 $url_components['path'] = str_replace('%2B', '+', $url_components['path']);
874
875 $url = str_replace($old_path, $url_components['path'], $url);
876 return $url;
877 }
878
879 /**
880 * Convert fv_player_encoding_category ID to a nice folder name.
881 * If you have:
882 * - Documentaries
883 * -- Nature
884 * --- Wildlife & Adventure
885 *
886 * you get: Documentaries/Nature/Wildlife-Adventure
887 *
888 * @param string $url Source video file URL
889 *
890 * @return string Sanitized file URL
891 */
892 function util__category_id_to_folder( $category_id ) {
893 $folder = false;
894
895 $category = get_term($category_id);
896 if( !is_wp_error($category) ) {
897 $hierarchy = array( $this->util__escape_filename($category->name) );
898 $ancestors = get_ancestors( $category->term_id, 'fv_player_encoding_category', 'taxonomy' );
899 foreach( (array)$ancestors as $ancestor ) {
900 $ancestor_term = get_term($ancestor, 'fv_player_encoding_category');
901 $hierarchy[] = $this->util__escape_filename($ancestor_term->name);
902 }
903 $hierarchy = array_reverse($hierarchy);
904 $folder = implode('/', $hierarchy);
905 }
906
907 return $folder;
908 }
909
910 /**
911 * Get sanitized file path. For example https://cdn.site.com/lessons/music/composing/lesson-1.mp4 gives you /lessons/music/composing/lesson-1
912 *
913 * @param $string Filename or URL
914 * @return string
915 */
916 function util__sanitize_target( $target ) {
917
918 $target = trim($target);
919
920 // take path only if it's full URL
921 $parsed = wp_parse_url($target);
922
923 if( !empty($parsed['scheme']) ) $target = str_replace($parsed['scheme'].'://', '', $parsed);
924 if( !empty($parsed['hostname']) ) $target = str_replace($parsed['hostname'], '', $parsed);
925
926 $target = preg_replace( '~/$~', '', $target ); // remove trailing slash
927
928 // sanitize filename
929 $target = explode('/', $target);
930
931 // deal with %20 encoding of spaces
932 $target = array_map( 'urldecode', $target );
933
934 $filename = $target[ count($target) - 1 ];
935
936 // remove file extension
937 if( strrpos( $filename, ".") ) {
938 $filename = substr( $filename, 0, strrpos( $filename, "."));
939 }
940
941 $filename = $this->util__escape_filename($filename);
942
943 // we're done
944 $target[ count($target) - 1 ] = $filename;
945 $target = join('/', $target);
946
947 return $target;
948 }
949
950 /**
951 * Sends an e-mail about changes in the encoding job.
952 *
953 * @param $id int int ID of the job to send this e-mail about.
954 * @param $author_id int ID of the author of the encoding job.
955 * @param $status string Status of the processed encoding job.
956 * @param $target string The actual target for the processed encoding job.
957 * @param $result string Text representation of the result, used to send any error messages along with the e-mail.
958 */
959 function send_email( $id, $author_id, $status, $target, $result ) {
960 $user = get_userdata( $author_id );
961 $to = $user->user_email;
962 $headers = array('Content-Type: text/plain; charset=UTF-8');
963
964 $subject = "[". get_bloginfo( 'name' ) . "] FV Player {$this->encoder_name}: Job #" . $id . " " . $target . " " . $status ;
965
966 $body = "Hello " . $user->display_name . ",\r\n";
967 $body .= "Your encoding job #" . $id . " " . $target . " has ";
968
969 if( $status == 'completed' ) {
970 $body .= "successfully finished.\r\n";
971 } else {
972 $body .= "run into some problems.\r\n";
973 $body .= $result."\r\n";
974 }
975
976 if ( user_can( $author_id, 'manage_options' ) ) {
977 $body .= "\r\nManage video encoding jobs <a href='". admin_url( 'admin.php?page=' . $this->encoder_wp_url_slug ) ."'>here</a>";
978 }
979
980 wp_mail( $to, $subject, $body, $headers );
981 }
982
983 /**
984 * Updates DB table definition for the extending plugin.
985 * Used when a version change of the extending plugin is detected,
986 * as well as displaying jobs listing page.
987 */
988 public function plugin_update_database() {
989 global $wpdb;
990
991 $sql = "CREATE TABLE ". $this->table_name ." (
992 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
993 id_video bigint(20) unsigned NOT NULL,
994 job_id varchar(45) NOT NULL,
995 date_created datetime NOT NULL,
996 date_checked datetime NOT NULL,
997 source varchar(1024) NOT NULL,
998 target varchar(1024) NOT NULL,
999 encryption BOOLEAN DEFAULT FALSE,
1000 trailer BOOLEAN DEFAULT FALSE,
1001 type varchar(64) NOT NULL,
1002 status varchar(64) NOT NULL,
1003 progress varchar(64),
1004 error varchar(1024),
1005 mime varchar(64),
1006 args TEXT,
1007 result TEXT,
1008 output TEXT,
1009 video_data TEXT,
1010 author bigint(20) unsigned NOT NULL default '0',
1011 fv_player_encoding_category_id bigint(20) unsigned DEFAULT NULL,
1012 PRIMARY KEY (id),
1013 KEY source (source(191)),
1014 KEY type (type),
1015 KEY status (status),
1016 KEY job_id (job_id(15))
1017 )" . $wpdb->get_charset_collate() . ";";
1018
1019 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
1020 dbDelta( $sql );
1021 }
1022
1023 /**
1024 * Displays general notices on top in Admin pages.
1025 */
1026 abstract function admin_notices();
1027
1028 /**
1029 * Returns an updated $conf variable with settings for the extending Encoder class.
1030 *
1031 * @param $conf Pre-populated configuration array into which the extending Encoder's class configuration should go.
1032 *
1033 * @return array Returns an updated $conf variable with settings for the extending Encoder class.
1034 */
1035 abstract function default_settings( $conf );
1036
1037 /**
1038 * Verifies the currently used endpoint supported by the extending Encoder, such as (S)FTP or S3 credentials
1039 * and either directly outputs a JSON-formatted error (for AJAX purposes) or returns the error to be processed further.
1040 *
1041 * @return mixed Returns TRUE if the current endpoint is set up properly, an error object/array otherwise.
1042 * If we're running an AJAX request, this method must return a valid JSON-formatted error for that request
1043 * by utilizing the wp_send_json() method in this format: wp_send_json( array('error' => $error) );
1044 */
1045 protected abstract function verify_active_endpoint( $target );
1046
1047 /*
1048 * Creates default Encoder's configuration.
1049 */
1050 abstract function get_conf( $args );
1051
1052 /**
1053 * Determines whether this Encoder has been properly configured.
1054 */
1055 abstract function is_configured();
1056
1057 /**
1058 * Prepares and returns data to be inserted into the "output" column of this encoder's DB table.
1059 */
1060 abstract protected function prepare_job_output_column_value();
1061
1062 /**
1063 * Retrieves new encoding job expiration time, used in URL signatures / tokens.
1064 *
1065 * @param $ttl An optional TTL parameter.
1066 *
1067 * @return int Returns the duration in seconds for which this job is valid.
1068 */
1069 abstract public function job_create_expiration( $ttl );
1070
1071 /**
1072 * Update job status
1073 *
1074 * @param object|int $pending_job Table row from encoder's table or its job ID
1075 *
1076 * @global object $wpdb WordPress database object
1077 * @global object $fv_fp FV Player
1078 *
1079 * @return array
1080 * array(
1081 * 'result' object Job info from the Encoder
1082 * 'status' string Valid values are: "processing", "completed", "error"
1083 * 'output' object URLs for all processed resources (such as video qualities, thumbnails etc.)
1084 * )
1085 */
1086 abstract protected function job_check( $pending_job );
1087
1088 /**
1089 * Submits the job to the Encoder service and stores the result in a table.
1090 *
1091 * @param int $job_id Job ID
1092
1093 * @global object $wpdb WordPress database object
1094 * @global object $fv_fp FV Player instance to load options with
1095 *
1096 * @return bool Result
1097 */
1098 abstract function job_submit( $id );
1099
1100 /**
1101 * Displays the jobs listing page contents.
1102 */
1103 abstract function tools_panel_jobs();
1104
1105 /**
1106 * Displays the Encoder's settings page contents.
1107 */
1108 abstract function tools_panel_settings();
1109
1110 /**
1111 * Must return __FILE__ from the extending class.
1112 * Used to determine plugin path for registering JS and CSS.
1113 */
1114 abstract function getFILE();
1115
1116 /**
1117 * Send out an e-mail notification of an encoding job change.
1118 * To be used when a WebHook is fired from the Encoding service.
1119 */
1120 abstract function email_notification();
1121
1122 }
1123