I have a webcam that write every minutes or so, an image to an ftp folder on the server where WP is installed. My question is how to get the last image displayed on page, with refresh every 60 second, and delete the older ones in that folder.
-
Can you tell me where my question could be more relevant? I cannot find another community on SO related to wordpress, so I ask here. By the way I don't asked for a plugin (if there is one would be better) but to get get some leads to solve a problem.– Valentino Dell'AicaCommented Sep 18, 2014 at 8:24
-
1Media files are handled by WordPress, uploads and deletions inclusive. This is not off topic.– fuxia ♦Commented Sep 18, 2014 at 9:27
-
2Have you seen the heartbeat-api already? And do you know about ajax in WordPress?– kaiserCommented Sep 18, 2014 at 10:11
-
2@toscho, it is an upload via ftp. this require a cron job and wordpress probably will not even have the permisions to delete image. At the least a wordpress based solution is the harder way to implement this.– Mark KaplunCommented Sep 18, 2014 at 11:56
-
Does the image need to be registered in the media library?– BODA82Commented Nov 17, 2014 at 17:56
1 Answer
Alright, here is code for having wordpress pages redraw an image on a page as new images are detected (like for a directory that a web cam automatically uploads images to). It assumes the following:
- You are using PHP 5.3 or higher (easily modified to not require it though)
- You have your web cam images uploading somewhere reachable directly via the web
- You don't want to put multiple cam images on a single page or post
I'm sure it has a lot of room for improvement, but it does the following:
- Lets you set up different cam pages
Section 1 is the main plugin file. It just needs to be placed in a php file, given a plugin header, and put in the root of an appropriate plugin folder.
Section 2 is the javascript file. The main plugin file assumes that it is in a subdirectory named js
and has the file name web_cam_checker.js
.
Section 1: The PHP File
/*
Plugin Name: Whatever You Want Here
Description: Handles updating an image via the heartbeat api for a web cam - in answer to a question on stack exchange
Version: 0.0.1
Author: The Privateer
*/
namespace Privateer\WebCam;
try {
if ( class_exists('Privateer_Do_Web_Cam_Updates') ) {
throw new \Exception('Privateer_Do_Web_Cam_Updates already exists!', 10001);
} else {
class Privateer_Do_Web_Cam_Updates{
protected $images_dir; # file system url images are uploaded to
protected $images_url; # web uri images are available at
protected $image_tag_id; # id of image to be swapped out on displayed page
protected $do_purge_images; # should old files be deleted
protected $refresh_interval_s; # how often the cam should refresh
protected $is_debug; # boolean - use debug mode?
protected $init_retry_ms; # Time in seconds to wait for initialization each try
protected $min_init_retries; # Maximum number of attempts to wait for initialization before quitting
protected $notices; # Any notices issued
protected $errors; # Any errors
function __construct(
$image_tag_id = '',
$images_dir = '',
$images_url = '',
$refresh_interval_s = 0,
$init_retry_ms = 0,
$min_init_retries = 0,
$is_debug = false,
$do_purge_images = false
) {
$this->notices = array();
$defaults = $this->get_default_settings();
$this->images_dir = ( empty($images_dir) )? $defaults['images_dir'] : (string) $images_dir;
$this->validate_images_dir_or_throw();
$images_url = ( empty($images_url) )? $defaults['images_url'] : (string) $images_url;
if ( empty( $images_url ) ) {
throw new \Exception("URL [{$images_url}] not found. Use _privateer_web_cam_images_url filter to set properly.", 10001);
} else {
$this->images_url = $images_url;
}
$image_tag_id = ( empty($image_tag_id) ) ? $defaults['image_tag_id'] : (string) $image_tag_id;
if ( empty($image_tag_id) ) {
throw new \Exception("Image Tag ID empty. Please fix via _privateer_web_cam_image_tag_id filter.", 10001);
} else {
$this->image_tag_id = $image_tag_id;
}
$do_purge_images = ( empty($do_purge_images) ) ? $defaults['purge_old_images'] : (bool) $do_purge_images;
$this->do_purge_images = ( $do_purge_images === true )? true : false;
# Limitations imposed by wp.heartbeat
$refresh_interval_s = ( empty( $refresh_interval_s ) )? $defaults['refresh_interval_seconds'] : (int) $refresh_interval_s;
if ( 5 > $refresh_interval_s ) {
$this->notices[] = "Min Refresh Interval is 5 seconds. Adjusted from {$refresh_interval_s} to 5.";
$this->refresh_interval_s = 5;
} else if ( 120 < $refresh_interval_s ) {
$this->notices[] = "Max Refresh Interval is 120 seconds. Adjusted from {$refresh_interval_s} to 120.";
$this->refresh_interval_s = 120;
} else {
$this->refresh_interval_s = $refresh_interval_s;
}
$is_debug = ( is_null($is_debug) )? $defaults['debug'] : (bool) $is_debug;
$this->is_debug = ( $is_debug )? 1 : 0;
$init_retry_ms = ( empty( $init_retry_ms ) )? $defaults['init_retry_ms'] : (int) $init_retry_ms;
if ( 200 > $init_retry_ms ) {
$this->notices[] = "Init Retry Time mimimum is 200 milliseconds. Adjusted from {$init_retry_ms} to 200.";
$this->init_retry_ms = 200;
} else {
$this->init_retry_ms = $init_retry_ms;
}
$min_init_retries = ( empty( $min_init_retries ) )? $defaults['init_min_retries'] : (int) $min_init_retries;
if ( 1 > $min_init_retries ) {
$this->notices[] = "Min Init Retries is 1. Adjusted from {$min_init_retries} to 1.";
$this->min_init_retries = 1;
} else {
$this->min_init_retries = $min_init_retries;
}
}
protected function get_default_settings() {
return array(
'images_dir' => plugin_dir_path( __FILE__ ) . 'cam-images',
'images_url' => plugin_dir_url( __FILE__ ) . 'cam-images',
'image_tag_id' => 'main_cam_image',
'purge_old_images' => false,
'refresh_interval_seconds' => 30,
'debug' => WP_DEBUG,
'init_retry_ms' => 500,
'init_min_retries' => 10
);
}
protected function validate_images_dir_or_throw() {
if ( !is_dir( $this->images_dir ) ) {
throw new \Exception("Directory [{$this->images_dir}] not found. Use _privateer_web_cam_images_dir filter to set properly.", 10001);
} else if ( !is_readable( $this->images_dir) ) {
throw new \Exception("Directory [{$this->images_dir}] not readable.", 10001);
}
}
# The function that processes received heartbeats via ajax
# - response: what we will be sending back (filtered)
# - data: what we received
# - screen_id: will be 'front' or an admin page
# Anything returning an error key will tell the javascript to stop
public function do_process_heartbeat_received( $response, $data, $screen_id ) {
$r = array();
$key = 'web_cam_checker_' . $this->image_tag_id;
if ( 'front' !== "{$screen_id}" ) {
$r['error'] = 'Not on front end of site.';
} else if ( !array_key_exists($key, $data) ) {
$r['error'] = "Failed to locate key {$key} in data received";
} else if ( !array_key_exists('current_image_src', $data["{$key}"]) ) {
$r['error'] = "Did not find current_image_src in {$key} data";
} else {
$current = $this->get_current_web_cam_image();
$reported = (string) $data["{$key}"]['current_image_src'];
if ( "{$current}" == "{$reported}" ) {
$r['notice'] = 'Image has not changed';
} else {
$r['webcam_new_uri'] = "{$current}";
}
}
$response["{$key}"] = $r;
return $response;
}
protected function get_readable_images_in_image_dir() {
$this->validate_images_dir_or_throw();
$images = array();
if ( $handle = opendir( "{$this->images_dir}" ) ) {
while ( false !== ( $file_name = readdir( $handle ) ) ) {
switch ( "{$file_name}" ) {
case '.':
case '..':
# Skip current and previous directory links
break;
default:
# Build the full file path to the file found
$file_path = "{$this->images_dir}/{$file_name}";
if ( is_file( "{$file_path}" ) && is_readable( "{$file_path}" ) ) {
# TODO: Check to be sure it is an image
$images["{$file_name}"] = $file_path;
}
break;
}
}
@closedir( $handle );
} else {
$this->notices[] = "Failed to open directory {$this->images_dir} for reading.";
}
return $images;
}
protected function get_newest_image_name($images) {
$newest_name = '';
$newest_ts = 0;
foreach ( $images as $name => $path ) {
$last_modified = filectime( $path );
if ( $last_modified > $newest_ts ) {
$newest_name = $name;
$newest_ts = $last_modified;
}
}
return $newest_name;
}
protected function get_current_web_cam_image() {
$current = ''; # The newest image on the web server
try {
$this->validate_images_dir_or_throw();
$images = $this->get_readable_images_in_image_dir();
if ( 0 < count($images) ) {
$newest_name = $this->get_newest_image_name($images);
$current = "{$this->images_url}/{$newest_name}";
if ( $this->do_purge_images ) {
$this->purge_older_images($images, $newest_name);
}
}
} catch ( \Exception $e ) {
$this->append_exception( $e );
$code = $e->getCode();
$message = $e->getMessage();
$trace = $e->getTraceAsString();
$line = $e->getLine();
$file = $e->getFile();
$err = new \WP_Error( "Error: {$file}(line {$line}): {$code} {$message}", $trace );
# You can hook into this to log errors somewhere if wanted
do_action('_privateer_do_web_cam_updates_error', $err);
}
return $current;
}
protected function append_exception( \Exception $e ) {
$code = $e->getCode();
$message = $e->getMessage();
$trace = $e->getTraceAsString();
$line = $e->getLine();
$file = $e->getFile();
$this->errors[] = new \WP_Error( "Error: {$file}(line {$line}): {$code} {$message}", $trace );
}
protected function purge_older_images( $images, $newest_image ) {
foreach ( $images as $file_name => $to_remove ) {
if ( "{$file_name}" !== "{$newest_image}" ) {
if( is_file( "{$to_remove}" ) && is_writeable( "{$to_remove}" ) ) {
if ( $this->is_debug ) {
$this->notices[] = "Would now be removing {$to_remove}";
} else {
$removed = unlink( "{$to_remove}" );
if ( !$removed ) {
$this->notices[] = "Failed to remove image: {$to_remove}";
}
}
}
}
}
}
# Use the _privateer_web_cam_loading filter to get the script to load where wanted
public function do_setup_javascript() {
$do_js = apply_filters('_privateer_web_cam_loading', false, $this->image_tag_id);
if ( $do_js ) {
add_action('get_header', array($this, 'do_register_js') );
add_action('wp_head', array($this, 'do_enqueue_js'));
}
}
public function do_register_js() {
wp_register_script('privateer_web_cam', plugins_url( '/js/web_cam_checker.js', __FILE__ ), array( 'jquery', 'heartbeat' ), "0.0.2", true);
}
public function do_enqueue_js() {
$web_cam_config = array(
'image_id' => "{$this->image_tag_id}",
'refresh_interval' => (int)$this->refresh_interval_s,
'debug' => (int) $this->is_debug,
'init_retry_ms' => $this->init_retry_ms,
'min_init_retries' => $this->min_init_retries
);
wp_localize_script('privateer_web_cam', 'pri_web_cam_settings', $web_cam_config );
wp_enqueue_script('privateer_web_cam');
}
function __destruct() {
do_action('_privateer_web_cam_runtime_errors', $this->errors);
do_action('_privateer_web_cam_runtime_notices', $this->notices);
}
}
function do_choose_privateer_web_cam_where_to_load($load, $image_id) {
if ( 'main_cam_image' == "{$image_id}" && is_front_page() ) {
$load = true;
}
return $load;
}
add_filter( '_privateer_web_cam_loading', '\\Privateer\\WebCam\\do_choose_privateer_web_cam_where_to_load', 10, 2);
# Create up an object to handle the web cam and provide wanted defaults
# Do this multiple times if you will be using different cam directories and/or image tags
$o_privateer_web_cam = new Privateer_Do_Web_Cam_Updates(
'main_cam_image', '', '', 0, 0, 0, true, false
);
if ( is_a( $o_privateer_web_cam, '\Privateer\WebCam\Privateer_Do_Web_Cam_Updates' ) ) {
# Set up the ajax responses
add_filter( 'heartbeat_received', array($o_privateer_web_cam, 'do_process_heartbeat_received'), 10, 3 );
add_filter( 'heartbeat_nopriv_received', array($o_privateer_web_cam, 'do_process_heartbeat_received'), 10, 3 );
# Set up the javascript for the front end on templates that you want it used on
if ( !is_admin() ) {
add_action( 'get_header', array($o_privateer_web_cam, 'do_setup_javascript'), 9 );
}
} else {
throw new \Exception('Failed to create Privateer_Do_Web_Cam_Updates object', 10001);
}
}
} catch ( \Exception $e ) {
$code = $e->getCode();
$message = $e->getMessage();
$trace = $e->getTraceAsString();
$line = $e->getLine();
$file = $e->getFile();
$err = new \WP_Error( "Error: {$file}(line {$line}): {$code} {$message}", $trace );
do_action('_privateer_web_cam_init_errors', $err);
if ( WP_DEBUG ) {
wp_die( "Error in {$file} on line {$line}: Code:{$code}, Message: {$message}, Trace: {$trace}" );
}
}
The Constructor Parameters:
- image_tag_id: The value in the id attribute of the image to display via
- images_dir: Full path to directory on server holding the images for this cam
- images_url: World reachable URL images can be viewed at
- refresh_interval_s: Seconds between heartbeats for this cam
- init_retry_ms: During init, how many milliseconds to wait between retries
- min_init_retries: Minimum tries to make at initialization before giving up
- is_debug: Whether or not to show debug messages on the js console
- do_purge_images: Whether or not to delete older images in the images_dir
Note: images_dir and images_url assume you created a cam-images under the root directory of the plugin. You can set them to whatever you want though.
Choosing which pages need the cam javascript
I chose to default it to not load the javascript at all and use a filter to let the user choose which pages need the script.
function do_choose_privateer_web_cam_where_to_load($load, $image_id) {
if ( 'main_cam_image' == "{$image_id}" && is_front_page() ) {
$load = true;
} else if ( 'second_cam_image' == "{$image_id}" && is_page('cam_two') ) {
$load = true;
}
return $load;
}
add_filter( '_privateer_web_cam_loading', '\\Privateer\\WebCam\\do_choose_privateer_web_cam_where_to_load', 10, 2);
Notice that I changed this one to assume two cam objects are being loaded:
- One having id="main_cam_image" on the front page
- Another having id="second_cam_image" on a page with the slug 'cam_two'
Tweak as wanted with the various is_* functions within wordpress to get the javscript file to load where wanted.
Other than that, the main setup is immediately following it.
If you want to create a cam object elsewhere (say a functions.php file), you would want to be sure to use the namespace (like the following):
$o_cam_two = new \Privateer\WebCam\Privateer_Do_Web_Cam_Updates(
'cam_two', '', '', 0, 0, 0, true, false
);
if ( is_a( $o_cam_two, '\Privateer\WebCam\Privateer_Do_Web_Cam_Updates' ) ) {
# Set up the ajax responses
add_filter( 'heartbeat_received', array($o_cam_two, 'do_process_heartbeat_received'), 10, 3 );
add_filter( 'heartbeat_nopriv_received', array($o_cam_two, 'do_process_heartbeat_received'), 10, 3 );
# Set up the javascript for the front end on templates that you want it used on
if ( !is_admin() ) {
add_action( 'get_header', array($o_cam_two, 'do_setup_javascript'), 9 );
}
}
Section Two: The Javascript File Any tips would be greatly appreciated from those more familiar with javascript. I'm just learning it, but did the best I could. It works ... and that's something.
jQuery(document).ready(function($) {
(function( document, config ) {
var settings = {
$cam_image: null,
cam_data: {
current_image_src: null
},
image_id: config.image_id,
debug: parseInt( config.debug ),
document: document,
tick_interval: parseInt( config.refresh_interval ),
waited: 0,
max_wait: parseInt( config.init_retry_ms ),
wait_delay_s: parseInt( config.min_init_retries )
};
function do_trigger(type, caller, problem ) {
console.log('Triggering ' + type + ', Caller: ' + caller + ', Problem: ' + problem);
if ( 'warning' === type ) {
settings.$document.trigger('web-cam-warning', caller + ': ' + problem);
} else {
settings.$document.trigger('web-cam-error', caller + ': ' + problem);
}
}
function do_enqueue_image(data) {
console.log('Trying to enqueue image...');
if ( ! wp.heartbeat.enqueue('web_cam_checker_' + settings.image_id, data, true ) ) {
do_trigger('error', 'do_enqueue_image', 'Failed to add to wp.heartbeat.enqueue. Data: ' + JSON.stringify( data ));
} else if ( settings.debug ) {
console.log( 'Queued: ' + JSON.stringify( wp.heartbeat.getQueuedItem('web_cam_checker_' + settings.image_id) ) );
}
}
function do_process_response(el, data) {
if ( settings.debug ) {
console.log( 'process_response:' );
console.log( '######\n ' + 'el: ' + JSON.stringify(el) + '\n######' );
console.log( '######\n ' + 'data: ' + JSON.stringify(el) + '\n######' );
}
if ( data['webcam_new_uri'] ) {
if ( settings.debug ) {
console.log('Found webcam_new_uri: ' + data['webcam_new_uri']);
}
settings.cam_data.current_image_src = data['webcam_new_uri'] + '';
settings.$cam_image.prop('src', settings.cam_data.current_image_src);
var worked = do_swap_current_image();
if ( worked ) {
if ( settings.debug ) {
console.log( 'Swam image worked, setting up next heartbeat queue.' );
}
do_enqueue_image(settings.cam_data);
}
} else {
if ( data['notice'] ) {
if ( settings.debug ) {
console.log('Notice Received: ' + data['notice'] + '\nSetting up next heartbeat queue.');
}
do_enqueue_image(settings.cam_data);
} else if ( data['error'] ) {
do_trigger('error', 'do_process_response', data['error']);
}
if ( settings.debug ) {
console.log('Full Data: ' + JSON.stringify(data) );
}
}
}
function do_swap_current_image() {
var worked = false;
if ( settings.debug ) {
console.log('attempting image swap');
}
var updated_src = settings.cam_data.current_image_src;
$("<img/>")
.one('load', function() {
if ( settings.debug ) {
console.log('Finished updating to ' + $(this).prop('src'));
}
worked = true;
})
.prop('src', updated_src )
.each(function(){
if ( this.complete ) {
$(this).trigger('load');
} else {
//do_trigger('error', 'do_swap_current_image', 'Did not finish updating to ' + $(this).prop('src'));
worked = true
}
});
return worked;
}
function do_setup_timeout( waiting_on ) {
settings.waited += 1;
if ( settings.waited < settings.max_wait ) {
setTimeout( do_init(), settings.wait_delay_s * 1000 );
} else {
do_trigger('error', 'do_setup_timeout', 'Giving up on ' + waiting_on + ' (waited ' + settings.waited + ' times)');
}
}
function do_init() {
if ( typeof window.wp === 'undefined' ) {
do_setup_timeout('window.wp');
} else if ( typeof window.wp.heartbeat === 'undefined' ) {
do_setup_timeout('window.wp.heartbeat');
} else if ( typeof settings.image_id === 'undefined' ) {
do_trigger('error', 'do_init', 'Cannot start web cam without html image tag id name');
} else {
settings.$cam_image = $('#' + settings.image_id);
console.log('Settings:' + JSON.stringify(settings.$cam_image));
if ( 0 === settings.$cam_image.length ) {
do_trigger('error', 'do_init', 'Failed to locate image #' + settings.image_id);
} else {
if ( settings.interval < 5 ) {
do_trigger('warning', 'do_init', 'Interval cannot be shorter than 5 seconds. Detected as ' + settings.interval );
settings.interval = 5;
} else if ( settings.interval > 120 ) {
do_trigger('warning', 'do_init', 'Interval cannot be longer that 120 seconds. Detected as ' + settings.interval );
settings.interval = 120;
}
settings.cam_data.current_image_src = settings.$cam_image.prop('src');
console.log('Settings Now: ' + JSON.stringify( settings ));
do_enqueue_image( settings.cam_data );
document.on('heartbeat-send', function(el, data) {
if ( settings.debug ) {
console.log('Data sent was ' + JSON.stringify( data ));
}
}).on('heartbeat-tick.web_cam_checker_' + settings.image_id, function(el, data) {
console.log('detected heartbeat tick:' + JSON.stringify(el));
if ( data.hasOwnProperty('web_cam_checker_' + settings.image_id) ) {
if ( settings.debug ) {
console.log('Data has web_cam_checker_' + settings.image_id);
}
do_process_response(el, data['web_cam_checker_' + settings.image_id]);
} else if ( settings.debug ) {
console.log('Data lacks web_cam_checker_' + settings.image_id + ': ' + JSON.stringify(data) );
}
});
wp.heartbeat.interval(settings.tick_interval);
}
}
}
do_init();
})( $(document), pri_web_cam_settings );
$(document)
.on('web-cam-error', function(e) {
console.log('Web Cam Error: ' + e);
})
.on('web-cam-warning', function(e) {
console.log('Web Cam Warning: ' + e);
})
.on('heartbeat.error', function(e) {
console.log('Heartbeat Error: ' + JSON.stringify(e) );
});
});
Thanks for the idea Kaiser. I hadn't heard of the heartbeat API and I was looking for something to try by growing javascript knowledge on ... so this was a good exercise.
I've only tried it on an LAMP server browsing from Firefox ... and no, I haven't put a strict declaration in yet ... but might do that the next time I have some time.
Anywise, hopefully this will help someone out a bit.
Any for those new to code...
To get this working as is:
- Create a directory in your wp-content/plugins directory (name it whatever, I'll use privateer-web-cam-updates)
- Create a new text file named privateer-web-cam-updates.php in that directory and open it in a plain text editor
- Add an id="main_cam_image" to an image tag on your site (say your front page). It might look like
<img src="#" id="main_cam_image" title="My Web Cam" />
- Edit the following section of code in the plugin.
function do_choose_privateer_web_cam_where_to_load($load, $image_id) {
if ( 'main_cam_image' == "{$image_id}" && is_front_page() ) {
$load = true;
}
return $load;
}
add_filter( '_privateer_web_cam_loading', '\\Privateer\\WebCam\\do_choose_privateer_web_cam_where_to_load', 10, 2);
If you have your image on a blog, change is_front_page()
to is_home()
.
If you have it on a page, get the page id and then change is_front_page()
to is_page(n)
where n is the ID of the page.
- Create a cam-images subdirectory in the plugin folder and set your web cam to places images there.
- Create a js subdirectory in the plugin folder.
- Copy the code from Section 2 into a new text file named web_cam_checker.js
And, again, constructive criticism is always appreciated. (cannot seem to get the last section to be detected as code for some reason)