/**
* Copyright 2014 Comcast Cable Communications Management, LLC
*
* This file is part of CATS.
*
* CATS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CATS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CATS. If not, see <http://www.gnu.org/licenses/>.
*/
package com.comcast.cats.video.service;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import org.apache.log4j.Logger;
import com.comcast.cats.event.CatsEventDispatcher;
import com.comcast.cats.event.VideoEvent;
/**
* Video Service implementation. Contains video service controls.
*
* @author JTyrre001
*
*/
public class VideoController implements Runnable
{
public static final String VIDEO_CGI_PATH = "/axis-cgi/mjpg/video.cgi";
public static final String IMAGE_CGI_PATH = "/axis-cgi/jpg/image.cgi";
/**
* Source object used when sending video events.
*/
public final Object source;
/**
* The regex used to extract the camera #.
*/
public static final Pattern CAMERA_PATTERN = Pattern.compile( ".*camera=([1-9]+).*" );
/**
* Default streaming rate in frames per second.
*/
private static final int DEFAULT_STREAM_FPS = 10;
private int imageSequence = 0;
/**
* Holds the current frames per second streaming frame rate.
*/
private int streamfps = DEFAULT_STREAM_FPS;
private AxisVideoSize VIDEO_DEFAULT_RESOLUTION = AxisVideoSize.AXIS_4CIF;
private AxisVideoSize currentVideoResolution = VIDEO_DEFAULT_RESOLUTION;
/**
* Instantiate AxisServerConnection.
*/
private final AxisServerConnection axis;
/**
* is Streaming flag.
*/
private boolean isStreaming = false;
/**
* Locator to video device.
*/
private final URI videoLocator;
/**
* CatsEventDispatcher object.
*/
private CatsEventDispatcher dispatcher;
/**
* Camera number defaults to 1;
*/
private Integer camera = 1;
/**
* default logger.
*/
private static final Logger logger = Logger.getLogger( VideoController.class );
protected void parseVideoLocator( URI videoLocator )
{
String query = videoLocator.getQuery();
if ( query == null )
{
return;
}
final Matcher m = CAMERA_PATTERN.matcher( query );
if ( m.find() )
{
camera = Integer.parseInt( m.group( 1 ) );
}
}
/**
* Constructor.
*
* @param parent
* parent
* @param url
* axis server url
* @throws MalformedURLException
*/
public VideoController( final CatsEventDispatcher dispatcher, final URI videoLocator ) throws MalformedURLException
{
if ( videoLocator == null )
{
logger.error( "VideoLocator is Null throwing IllegalArgumentException" );
throw new IllegalArgumentException( "VideoLocator is null which is not allowed" );
}
if ( dispatcher == null )
{
logger.error( "No CatsEventDispatcher so throwing IllegalArgumentException" );
throw new IllegalArgumentException( "CatsEventDispatcher is null which is not allowed" );
}
this.videoLocator = videoLocator;
this.source = this;
parseVideoLocator( videoLocator );
axis = new AxisServerConnection( generateVideoURL() );
this.dispatcher = dispatcher;
}
/**
* Constructor.
*
* @param parent
* parent
* @param url
* axis server url
* @param source
* source object for all events generated by this class.
* @throws MalformedURLException
*/
public VideoController( final CatsEventDispatcher dispatcher, final URI videoLocator, Object source )
throws MalformedURLException
{
if ( videoLocator == null )
{
logger.error( "VideoLocator is Null throwing IllegalArgumentException" );
throw new IllegalArgumentException( "VideoLocator is null which is not allowed" );
}
if ( dispatcher == null )
{
logger.error( "No CatsEventDispatcher so throwing IllegalArgumentException" );
throw new IllegalArgumentException( "CatsEventDispatcher is null which is not allowed" );
}
this.videoLocator = videoLocator;
this.source = source;
parseVideoLocator( videoLocator );
axis = new AxisServerConnection( generateVideoURL() );
this.dispatcher = dispatcher;
}
private String generateVideoURL()
{
return generateURL( VIDEO_CGI_PATH );
}
private String generateImageURL()
{
return generateURL( IMAGE_CGI_PATH );
}
private String generateImageURL( AxisVideoSize videoSize )
{
String tempUrl = generateBaseURL( IMAGE_CGI_PATH );
tempUrl += getVideoResolutionString( videoSize );
return tempUrl;
}
private String generateBaseURL( String path )
{
String tempUrl = "http://" + videoLocator.getHost();
if ( videoLocator.getPort() > 0 )
{
tempUrl += ":" + videoLocator.getPort();
}
tempUrl += path;
tempUrl += "?camera=" + getCamera();
return tempUrl;
}
private String generateURL( String path )
{
String tempUrl = generateBaseURL( path );
tempUrl += getCurrentVideoResolutionString();
tempUrl += "&fps=" + getFrameRate();
logger.debug( "Generated URL " + tempUrl );
return tempUrl;
}
public Integer getCamera()
{
return camera;
}
/**
* Returns Axis server URL.
*
* @return URL of Axis Server device
*/
public String getURL()
{
return generateVideoURL();
}
public URI getVideoLocator()
{
return videoLocator;
}
/**
* Returns streaming video status of true or false.
*
* @return axis.isConnected() connected status. Presumes connected,
* therefore streaming.
*/
public boolean isStreaming()
{
return isStreaming;
}
/**
* Returns connection to Axis Server status of true or false.
*
* @return axis.isConnected() connected status. Presumes connected,
* therefore streaming.
*/
public boolean isConnected()
{
return axis.isConnected();
}
/**
* Connects to the Axis server device.
*
* @throws MalformedURLException
*/
public void connect() throws MalformedURLException
{
disconnectFromAxisDevice();
while ( isStreaming )
{ // wait till last thread returns
try
{
Thread.sleep( 10 );
}
catch ( InterruptedException e )
{
e.printStackTrace();
}
}
axis.setUrl( getURL() );
axis.connect();
Thread thread = new Thread( this );
thread.start();
}
/**
* Disconnects from the Axis Server device.
*/
public void disconnectFromAxisDevice()
{
if ( axis.isConnected() )
{
axis.disconnect();
}
}
private BufferedImage readImage( AxisVideoSize axisSize )
{
String imageLocation = generateImageURL( axisSize );
try
{
logger.info( "Retrieve Image[" + imageLocation + "]" );
URL imageURL = new URL( imageLocation );
return ImageIO.read( imageURL );
}
catch ( MalformedURLException e1 )
{
logger.error( "Error creating URL for image location = " + imageLocation );
}
catch ( IOException e )
{
logger.error( "Error retrieving Image from = " + imageLocation );
}
return null;
}
/**
* Returns a buffered image.
*
* @return BufferedImage
*/
public BufferedImage getImage()
{
if ( isStreaming )
{
/**
* Streaming is enabled, so try and pull image from axis server.
*/
logger.info( "Streaming video, pull image out of stream." );
BufferedImage image;
try
{
image = axis.getImage();
}
catch ( IOException e )
{
logger.debug( "Excpetion while redaing image " + e.getMessage() );
image = null;
}
return image;
}
logger.debug( "Reading image with current video resolution" );
return readImage( currentVideoResolution );
}
public BufferedImage getImage( Dimension dim )
{
if ( null == dim )
{
throw new IllegalArgumentException( "Dimension must not be null" );
}
AxisVideoSize axisSize = AxisVideoSize.getAxisFormatFromDimension( dim );
if ( null == axisSize )
{
logger.warn( "Dimension provided is invalid Axis Video Size " );
return null;
}
return readImage( axisSize );
}
/**
* Sets the frames per second read rate.
*
* @param fps
* frames per second read rate
*/
public void setFrameRate( int fps )
{
streamfps = ( fps <= 0 || fps > 30 ) ? DEFAULT_STREAM_FPS : fps;
if ( isConnected() )
{
try
{
connect();// reconnect.
}
catch ( MalformedURLException e )
{
e.printStackTrace();
}
}
}
/**
* Gets the frames per second read rate.
*
* @return streamfps frames per second read rate
*/
public int getFrameRate()
{
return streamfps;
}
/**
* Gets and returns an ArrayList (table) of available video resolutions
* sizes. First column is resolution id: D1, 4CIF, CIF etc. Second column is
* resolution: 720 x 480 etc.
*
* @return resList available video size dimension list
*/
public Collection< String > getAvailableVideoSizes()
{
return AxisServerConnection.getAvailableVideoResolutions();
}
/**
* Get the Axis URL string resolution based on currently selected
* resolution.
*
* @return Axis camera string resolution parameters.
*/
private String getCurrentVideoResolutionString()
{
logger.debug( "Getting Video Dimension " + currentVideoResolution.getAxisResolution() );
return getVideoResolutionString( currentVideoResolution );
}
private String getVideoResolutionString( AxisVideoSize axisSize )
{
String res = "&resolution=" + axisSize.getAxisResolution();
// if(currentVideoResolution.isSquareResoultion()) {
// res += "&squarepixel=1";
// }
if ( axisSize.isSquareResoultion() )
{
res += "&squarepixel=1";
}
return res;
}
/**
* Sets the height/width dimension of the user specified screen size.
*
* @param dm
* Dimension of selected screen resolution
*/
public void setVideoDimension( final Dimension dm )
{
currentVideoResolution = AxisVideoSize.getAxisFormatFromDimension( dm );
if ( currentVideoResolution == null )
{
currentVideoResolution = VIDEO_DEFAULT_RESOLUTION;
}
logger.debug( "Set Video Dimension to " + currentVideoResolution + " Requested : " + dm );
}
/**
* Gets and return the screen dimension set by the user client.
*
* @return dim client/user selected screen dimension.
*/
public Dimension getVideoDimension()
{
return currentVideoResolution.getDimension();
}
/**
* Thread to read frames out from the stream. (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
@Override
public void run()
{
readStream();
}
/**
* Reads and extracts JPEGS frames from the stream at a specified rate
* (frames per second).
*/
private void readStream()
{
try
{
while ( axis.isConnected() )
{
isStreaming = true;
BufferedImage image = axis.getImage();
VideoEvent videoEvent = new VideoEvent( imageSequence++, null, source, image );
dispatcher.sendCatsEvent( videoEvent );
}
if ( logger.isTraceEnabled() )
{
logger.trace( "Axis is no longer connected so VideoController is no longer streaming." );
}
isStreaming = false;
}
catch ( Exception e )
{
e.printStackTrace();
isStreaming = false;
logger.trace( "Exception occured so VideoController is no longer streaming." + e.getMessage() );
logger.debug( "Try Reconnecting " );
try
{
connect();
}
catch ( MalformedURLException e1 )
{
logger.debug( "Incorrect URL " + e1.getMessage() );
}
}
}
}