4

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'Aica Sep 18 '14 at 8:24
  • 1
    Media files are handled by WordPress, uploads and deletions inclusive. This is not off topic. – fuxia Sep 18 '14 at 9:27
  • 2
    Have you seen the heartbeat-api already? And do you know about ajax in WordPress? – kaiser Sep 18 '14 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 Kaplun Sep 18 '14 at 11:56
  • Does the image need to be registered in the media library? – BODA82 Nov 17 '14 at 17:56
1

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:

  1. You are using PHP 5.3 or higher (easily modified to not require it though)
  2. You have your web cam images uploading somewhere reachable directly via the web
  3. 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:

  1. One having id="main_cam_image" on the front page
  2. 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:

  1. Create a directory in your wp-content/plugins directory (name it whatever, I'll use privateer-web-cam-updates)
  2. Create a new text file named privateer-web-cam-updates.php in that directory and open it in a plain text editor
  3. 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" />
  4. 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.

  1. Create a cam-images subdirectory in the plugin folder and set your web cam to places images there.
  2. Create a js subdirectory in the plugin folder.
  3. 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)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.