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

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