/* * [NetworkCam.java] * * Summary: Displays a webcam jpg stream of images. * * Copyright: (c) 2002-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2002-07-27 * 1.1 2004-01-01 * 1.2 2005-01-01 * 1.3 2005-06-16 standardise compile bat files. * 1.4 2005-07-30 and build, move to JDK 1.2 for signing. * 1.5 2006-03-06 reformat with IntelliJ, add Javadoc * 1.6 2007-04-19 add flip, mirror and rotate. */ package com.mindprod.networkcam; import com.mindprod.common18.Misc; import com.mindprod.common18.ResizingImageViewer; import com.mindprod.common18.ST; import com.mindprod.common18.StoppableThread; import com.mindprod.common18.VersionCheck; import java.applet.Applet; import java.applet.AudioClip; import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.image.ImageProducer; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.Enumeration; import java.util.Random; import static java.lang.System.*; /** * Displays a webcam jpg stream of images. *

* Applet to display images from a webcam. The server presents jgp images, all with the same name that change very * frequently. We refresh the image regularly at a slow pace and at a faster rate for a while after the user clicks * anywhere on the image. Various alternative to this program are listed at: http://www.help4webcams.com/meta.shtml *

* * @author Roedy Green, Canadian Mind Products * @version 1.6 2007-04-19 * @noinspection FieldCanBeLocal * @since 2002-07-27 */ public final class NetworkCam extends Applet implements Runnable { /** * True if you want debugging output */ private static final boolean DEBUGGING = false; private static final int FIRST_COPYRIGHT_YEAR = 2002; /** * undisplayed copyright notice * * @noinspection UnusedDeclaration */ private static final String EMBEDDED_COPYRIGHT = "Copyright: (c) 2002-2017 Roedy Green, Canadian Mind Products, http://mindprod.com"; /** * @noinspection UnusedDeclaration */ private static final String RELEASE_DATE = "2007-04-19"; /** * embedded version string. * * @noinspection UnusedDeclaration */ private static final String VERSION_STRING = "1.6"; /** * add more possible frog sounds here: */ private static final String[] frogSounds = { // does NOT include cameraClick.au! "braak.au", "bullfrog.au", "bullfrog.au", "eeak.au", "greentoad.au", "greentreefrog.au", "peeper.au", "squee.au", "sunfrog.au", "tiny.au", }; /* TODO: worries: make sure always requesting fresh image. make sure always repainting fresh image. make sure not requesting images faster than they actually change. recover after loss of connection, or failure to connect in the first place. consider getting just header to detect date/size change. */ /** * Used to get various Applets out of sync. Same random stream shared by all Applets. */ private static final Random wheel = new Random(); /** * camera number of this instance of the Applet */ private static int cameraNumber; /** * shared variable to generate a camera number */ private static int uniqueGenerator = 0; /** * transform to flip, mirror, rotate the image. Null if nothing needed. */ private AffineTransform transform; /** * sound for click */ private AudioClip clickSound; /** * sound for refresh start */ private AudioClip frogSound; /** * Web cam jpg image. */ private Image camImage; /** * Component to display the Image */ private ResizingImageViewer camViewer; /** * Thread that triggers periodic reloads and repaints. Private because any meddling could tip the applecart. */ private StoppableThread ticker; /** * identifier for the camera, primarily for debugging. */ private String cameraName; /** * connection to the source of the JPG stream */ private URLConnection urlc; /** * url of the images */ private URL camURL; /** * true if the user recently clicked the image. We should refresh the image and go back to fast refresh. */ private volatile boolean clicked; /** * should we flip the image top to bottom? */ private boolean flip; /** * should we invert the image left to right? */ private boolean mirror; /** * whether we should play frog sounds */ private boolean quiet; /** * how many degrees should we rotate the image. +ve is counter clockwise, math convention. */ private int rotateDegrees; /** * Fast refresh rate in milliseconds per interval. */ private long refreshFast; /** * Slow refresh rate in milliseconds per interval. */ private long refreshSlow; /** * Refresh timeout in milliseconds before switching back to the slow rate. */ private long refreshTimeout; /** * Get unique camera number 1..n * * @return unique camera number, assigned sequentially. */ private static int getCameraNumber() { return ++uniqueGenerator; } /** * Get Applet optional boolean parameter * * @param paramName Name of the parameter. Case insensitive. * @param defaultValue default if param is missing. * * @return Value of the parameter from the Applet true or false * @noinspection SameParameterValue */ private boolean getBooleanParameter( final String paramName, final boolean defaultValue ) { final String boolString = getParameter( paramName ); if ( ST.isEmpty( boolString ) ) { return defaultValue; } else { return Misc.parseBoolean( boolString, false ); } } /** * Get Applet cameraname parameter * * @return Value of the parameter from the Applet tag, default is a generated name */ private String getCameraNameParameter() { String cameraName = getParameter( "cameraname" ); if ( cameraName == null ) { // generate a unique name cameraName = "camera-" + cameraNumber; } return cameraName; } /** * Get sound to indicate start of a refresh return audioClip to play a random frog sound. * * @return audio of a frog croak. */ private AudioClip getFrogSound() { /* pick a random frog sound 0..n-1 */ int frogNumber = wheel.nextInt( frogSounds.length ); URL u = NetworkCam.class.getResource( frogSounds[ frogNumber ] ); if ( u == null ) { throw new IllegalArgumentException( "com/mindprod/networkcam/" + frogSounds[ frogNumber ] + ".au is missing from networkcam.jar" ); } return getAudioClip( u ); } /** * Get Applet parameter that should have a numeric value. * * @param paramName Name of the parameter. Case insensitive. * @param low lowest legal value for parm. * @param defaultValue Default value for the parameter if it is missing or malformed. * @param high highest legal value for parm. * * @return Value of the parameter from the Applet tag, (or the default) */ private int getNumericParameter( String paramName, int low, int defaultValue, int high ) { String paramValue = null; try { paramValue = getParameter( paramName ); if ( paramValue == null ) { return defaultValue; } int result = Integer.parseInt( paramValue ); if ( result < low ) { err.println( "NetworkCam: value for " + paramName + " param: " + paramValue + " cannot be below " + low ); return defaultValue; } if ( result > high ) { err.println( "NetworkCam: value for " + paramName + " param: " + paramValue + " cannot be above " + high ); return defaultValue; } return result; } catch ( NumberFormatException e ) { err.println( "NetworkCam: invalid value for " + paramName + " param: " + paramValue ); return defaultValue; } } /** * Get Applet parameter that should have a URL value. * * @param paramName Name of the parameter. Case insensitive. * * @return Value of the parameter from the applet tag, null if not found, or malformed * @noinspection SameParameterValue */ private URL getURLParameter( String paramName ) { String urlString = null; try { urlString = getParameter( paramName ); if ( urlString == null ) { err.println( "NetworkCam: missing value for " + paramName + " param" ); return null; } else { return new URL( getDocumentBase(), urlString ); } } catch ( MalformedURLException e ) { throw new IllegalArgumentException( "NetworkCam: invalid value for " + paramName + " param: " + urlString ); } } /** * like getImage but ensures all fetches of content of it will be uncached * * @param url URL of the image, e.g. http://mindprod.com/image/roedy.jpg * * @return corresponding Image, no MediaTracker */ private Image getUncachedImage( URL url ) { try { // Open a Connection to the server urlc = url.openConnection(); if ( urlc == null ) { throw new IOException( "Unable to make a connection to the image source" ); } // Turn off caches to force fresh reload of the jpg urlc.setUseCaches( false ); urlc.connect(); // ignored if already connected. if ( DEBUGGING ) { long lastModified = urlc.getLastModified(); if ( lastModified != 0 ) { out.println( " age of incoming image: " + ( System.currentTimeMillis() - lastModified ) + " millis : " + cameraName ); } else { out.println( " no age on image : " + cameraName ); } } return createImage( ( ImageProducer ) ( urlc.getContent() ) ); } catch ( IOException e ) { err.println( "NetworkCam: server not responding at : " + url ); return null; } } /** * Are there other copies of this same Applet running on the same page? * * @return true if there are are other copies. */ private boolean isMultipleInstance() { for ( Enumeration other = getAppletContext().getApplets(); other .hasMoreElements(); ) { Object otherApplet = other.nextElement(); if ( otherApplet != this && otherApplet instanceof NetworkCam ) { return true; } } return false; } /** * Do one sleep, reload, repaint cycle. Will quit early if ticker.gentleStop() called asynchronously. * * @param delay in ms. */ private void oneCycle( long delay ) { // don't use wasRecentClick() here because we don't want it reset just // yet. if ( clicked || ticker.stopping() ) { return; } try { Thread.sleep( delay ); } catch ( InterruptedException e ) { // may be awakened early by click interrupt } if ( clicked || ticker.stopping() ) { return; } if ( DEBUGGING ) { out.println( "Time for a refresh: " + cameraName ); } refreshCamImage(); } /** * load and display the image Prevent simultaneous updates */ private void refreshCamImage() { if ( DEBUGGING ) { out.println( " starting refresh: " + cameraName ); } if ( !quiet ) { frogSound.play(); } // throw away the old Image contents, forcing a reload if ( camImage != null ) { camImage.flush(); } if ( DEBUGGING ) { long lastModified = urlc.getLastModified(); if ( lastModified != 0 ) { out.println( " age of incoming image: " + ( System.currentTimeMillis() - lastModified ) + " millis : " + cameraName ); } else { out.println( " no age on image : " + cameraName ); } } if ( DEBUGGING ) { long length = urlc.getContentLength(); out.println( " length of image: " + length + " bytes : " + cameraName ); } camViewer.setImage( camImage ); if ( DEBUGGING ) { out.println( " finishing refresh: " + cameraName ); } } /** * True if user recently clicked the image. Autoresetting. * * @return true if user recently clicked the image */ private boolean wasRecentClick() { if ( clicked ) { // reset so we won't process it twice clicked = false; return true; } else { return false; } } /** * Make sure we don't try to create the image until the Applet peer is ready. Just might not be ready in init. * Called after init. */ public void addNotify() { super.addNotify(); if ( camImage == null && camURL != null ) { camImage = getUncachedImage( camURL ); } } /** * Called by the browser or Applet viewer to inform * this Applet that it is being reclaimed and that it should destroy * any resources that it has allocated. */ public void destroy() { cameraName = null; camImage = null; camURL = null; camViewer = null; clickSound = null; frogSound = null; ticker = null; transform = null; urlc = null; } /** * Called by the browser or Applet viewer to inform * this Applet that it has been loaded into the system. */ public void init() { // has to be JDK 1.2+ for signing if ( !VersionCheck.isJavaVersionOK( 1, 8, 0, this ) ) { return; } cameraNumber = getCameraNumber(); cameraName = getCameraNameParameter(); quiet = getBooleanParameter( "quiet", true/* default */ ); // Click sound same for all instantiations. // Play where quiet true or false clickSound = getAudioClip( NetworkCam.class .getResource( "cameraclick.au" ) ); if ( !quiet ) { frogSound = getFrogSound(); } // get all Applet params name low default high // cannot use TimeUnit in Java 1.3 refreshFast = getNumericParameter( "RefreshFast", 1, 10, 3600 ) * 1000L; refreshSlow = getNumericParameter( "RefreshSlow", 1, 120, 3600 ) * 1000L; refreshTimeout = getNumericParameter( "RefreshTimeout", 1, 30, 3600 ) * 1000L; camURL = getURLParameter( "Image" ); flip = getBooleanParameter( "Flip", false/* default */ ); mirror = getBooleanParameter( "Mirror", false/* default */ ); rotateDegrees = getNumericParameter( "Rotate", -360, 0, 360 )/* low, default, high */; if ( camImage == null && camURL != null ) { camImage = getUncachedImage( camURL ); } if ( DEBUGGING ) { out.println( "size: " + getWidth() + "x" + getHeight() ); } // build Affine transform to flip, mirror and rotate. if ( flip || mirror || rotateDegrees != 0 ) { transform = new AffineTransform(); // Temporarily move the origin to the center of the image. // All transforms are symmetrical about this point. // Otherwise the image would rotate // around the upper left corner, and disappear off screen. transform.translate( getWidth() / 2.0, getHeight() / 2.0 ); if ( flip ) { /* turn image upside down */ transform.scale( 1.0, -1.0 ); } if ( mirror ) { /* mirror image left to right */ transform.scale( -1.0, 1.0 ); } if ( rotateDegrees != 0 ) { if ( rotateDegrees % 90 == 0 ) { /* rotate image in 90 degree chunks, +ve is anticlockwise */ transform.quadrantRotate( rotateDegrees / 90 ); } else { /* rotate image by some odd angle */ transform.rotate( Math.toRadians( rotateDegrees ) ); } } // put origin back to upper left corner transform.translate( -getWidth() / 2.0, -getHeight() / 2.0 ); } else { transform = null; } // no Layout, just one component setLayout( null ); setBackground( Color.black ); camViewer = new ResizingImageViewer( transform ); camViewer.setBackground( Color.black ); // maker camViewer fill entire Applet frame camViewer.setSize( this.getSize() ); camViewer.setLocation( 0, 0 ); // arrange if user clicks anywhere on viewer, we reload the image. camViewer.addMouseListener( new MouseAdapter() { // anonymous MouseAdapter class public void mouseClicked( MouseEvent m ) { if ( DEBUGGING ) { out.println( "Click: " + cameraName ); } clickSound.play(); /* restart the ticker at fast speed */ clicked = true; if ( ticker != null ) { // wake up sleeping timer thread try { ticker.interrupt(); } catch ( SecurityException e ) { err.println( "Security problem. Click ignored. " + e.getMessage() ); } } // end if // we don't do any work to avoid tying up the AWT thread. } // end mouseClicked } // end anonymous class );// end addWMouseListener add( camViewer ); this.validate(); this.setVisible( true );// Lights, Camera, Action. } /** * Trigger a periodic repaint and reload of the image. * * @noinspection UnnecessaryLabelOnContinueStatement */ public void run() { restart: while ( true ) { if ( ticker.stopping() ) { return; } if ( wasRecentClick() ) { continue restart; } // repaint image immediately to get started. if ( DEBUGGING ) { out.println( "Restarting : " + cameraName ); } refreshCamImage(); if ( ticker.stopping() ) { return; } if ( wasRecentClick() ) { continue restart; } // round timeout to closest even number of fastCycles. int fastCycles = ( int ) ( ( refreshTimeout + refreshFast / 2 ) / refreshFast ); if ( isMultipleInstance() && fastCycles > 0 ) { // Do one cycle at a random interval to help get multiple // Applets out of sync. // This cycle will be somewhat faster/shorter than usual. int random; // other Applet instances may be using the wheel synchronized ( wheel ) { // somewhere between 0 and normal refreshFast time. random = wheel.nextInt( ( int ) refreshFast ); } if ( DEBUGGING ) { out.println( "random cycle: " + random + " milliseconds " + cameraName ); } oneCycle( random ); if ( ticker.stopping() ) { return; } if ( wasRecentClick() ) { continue restart; } fastCycles--; } // do several cycles at the fast refresh rate, countdown loop for ( ; fastCycles > 0; fastCycles-- ) { oneCycle( refreshFast ); if ( ticker.stopping() ) { return; } if ( wasRecentClick() ) { continue restart; } } // repeat indefinitely at the slow rate while ( true ) { oneCycle( refreshSlow ); if ( ticker.stopping() ) { return; } if ( wasRecentClick() ) { continue restart; } } } // end restart loop } /** * start Applet up again */ public void start() { if ( ticker == null ) { ticker = new StoppableThread( this ); // invoke ticker.run method on a separate thread ticker.start(); } } /** * stop ticker thread gracefully */ public void stop() { if ( ticker != null ) { ticker.gentleStop( true, 0 ); // cannot reuse this thread after it has been stopped. // Free up considerable RAM for the thread for the browser // to use for something else. ticker = null; } } /** * bypass usual clear for speed since we will fill entire frame * * @param g graphics region to paint */ public void update( Graphics g ) { paint( g ); } } // end NetworkCam Applet