How to take snapshots of a transcoded stream at regular intervals (SnapShot)

Note: This article is for an older Wowza™ product or technology that either has been updated or is no longer supported. For the current version of this article, see How to take timed thumbnail snapshots of a Wowza Transcoder stream (TranscoderTimedSnapshot).
This example Wowza module demonstrates how to take snapshots of a transcoded stream at regular intervals. You must have Wowza Transcoder configured prior to using this module; otherwise, nothing will be detected.

The snapshots are stored in the StorageDir of the Application and are in the following format:

thumbnail-<Stream Name>-<time in seconds from epoch>
 
Note: Wowza Media Server™ 3.6.3 or later is required.

Configuration


Application Module

The following Application module should be added to Application.xml for the application that's running the transcoder.
package com.wowza.demo.transcoder.timed;

import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
import com.wowza.wms.application.*;
import com.wowza.wms.logging.WMSLogger;
import com.wowza.wms.module.*;
import com.wowza.wms.stream.IMediaStream;
import com.wowza.wms.transcoder.model.ITranscoderFrameGrabResult;
import com.wowza.wms.transcoder.model.LiveStreamTranscoder;
import com.wowza.wms.transcoder.model.TranscoderNativeVideoFrame;
import com.wowza.wms.transcoder.model.TranscoderStream;
import com.wowza.wms.transcoder.model.TranscoderStreamSourceVideo;
import com.wowza.wms.transcoder.util.TranscoderStreamUtils;

public class SnapShot extends ModuleBase {

	private SnapShotGet SnapShotTaker = null;
	private String StreamName = "";
	private int SnapShotHeight = 426;
	private int SnapShotWidth = 240;
	private int SnapShotTimer = 500;
	private WMSLogger Log = null;

	public void onAppStart(IApplicationInstance appInstance) 
		{

		this.Log = getLogger();

		this.StreamName = appInstance.getProperties().getPropertyStr("streamName", "myStream");
		this.SnapShotHeight = appInstance.getProperties().getPropertyInt("snapHeight", 426);
		this.SnapShotWidth = appInstance.getProperties().getPropertyInt("snapWidth", 240);
		this.SnapShotTimer = appInstance.getProperties().getPropertyInt("snapTime", 500);


		if ( this.SnapShotHeight < 20 )
			{
			this.Log.info("SnapShot Height reset, less than 20 , reset to 426");
			this.SnapShotHeight = 426;
			}

		if ( this.SnapShotWidth < 20 )
			{
			this.Log.info("SnapShot Width reset, less than 20 , reset to 240");
			this.SnapShotWidth = 240;
			}

		if ( this.SnapShotTimer < 1000 )
			{
			this.Log.info("SnapShot Timer reset, less than 1000 , reset to 1000, remember it is in milliseconds");
			this.SnapShotTimer = 1000;
			}

		this.SnapShotTaker = new SnapShotGet(this.StreamName,this.SnapShotHeight,this.SnapShotWidth,this.SnapShotTimer,appInstance,getLogger());
		this.SnapShotTaker.setDaemon(true);
		this.SnapShotTaker.start();


	}

	public void onAppStop(IApplicationInstance appInstance) 
	{

		if ( this.SnapShotTaker != null )
			{
			this.SnapShotTaker.quit();
			}
	}

	class CreateShot implements ITranscoderFrameGrabResult
	{

		private IApplicationInstance thisApp = null;
		private WMSLogger Log = null;
		private String StreamName = "";

		public CreateShot ( String streamName,IApplicationInstance appins,WMSLogger log)
		{
			this.StreamName = streamName;
			this.thisApp = appins;
			this.Log = log;
		}

		public void onGrabFrame(TranscoderNativeVideoFrame videoFrame)
		{


			BufferedImage image = TranscoderStreamUtils.nativeImageToBufferedImage(videoFrame);

			if (image != null)
			{
				long time = System.currentTimeMillis()/1000;

				String storageDir = this.thisApp.getStreamStoragePath();

				File pngFile = new File(storageDir+"/thumbnail-"+this.StreamName+"-"+time+".png");
				File jpgFile = new File(storageDir+"/thumbnail-"+this.StreamName+"-"+time+".jpg");

				try
				{
					if (pngFile.exists())
						pngFile.delete();
					ImageIO.write(image, "png", pngFile);
				}
				catch(Exception e)
				{
					this.Log.error("ModuleTestTranscoderFrameGrab.grabFrame: File write error: "+pngFile);
				}

				try
				{
					if (jpgFile.exists())
						jpgFile.delete();
					ImageIO.write(image, "jpg", jpgFile);
				}
				catch(Exception e)
				{
					this.Log.error("ModuleTestTranscoderFrameGrab.grabFrame: File write error: "+jpgFile);
				}
			}
		}
	}

	public class SnapShotGet extends Thread{

		private boolean running = true;
		private boolean quit = false;
		private WMSLogger Log = null;
		private IApplicationInstance appInstance = null;
		private String StreamName = "";
		private boolean TakeShot = true;
		private int SnapShotHeight = 0;
		private int SnapShotWidth = 0;
		private int SnapShotTimer = 0;

		public SnapShotGet(String stream,int height, int width,int timer,IApplicationInstance thisapp, WMSLogger mylog)
		{
		this.StreamName = stream;
		this.SnapShotHeight = height;
		this.SnapShotWidth = width;
		this.SnapShotTimer = timer;
		this.appInstance = thisapp;
		this.Log = mylog;
		}


		public synchronized boolean running()
		{
			return this.running;
		}

		public synchronized void quit()
		{
			this.quit = true;
			notify();
		}

		public void run()
		{

			if ( this.SnapShotHeight == 0 || this.SnapShotWidth == 0 )
				{
				this.Log.info("Snapshot size, width or height is 0, not running");
				this.running = false;
				return;
				}

			while(true)
				{

				IMediaStream stream = null;
				LiveStreamTranscoder liveStreamTranscoder = null;
				TranscoderStream transcodingStream = null;
				TranscoderStreamSourceVideo sourceVideo = null;


				stream = this.appInstance.getStreams().getStream(this.StreamName);
				if (stream == null)
					{
					this.TakeShot = false;
					}
				else
				{

				liveStreamTranscoder = (LiveStreamTranscoder)stream.getLiveStreamTranscoder("transcoder");
				}
				if (liveStreamTranscoder == null)
					{
					this.TakeShot = false;
					}
				else
				{
				transcodingStream = liveStreamTranscoder.getTranscodingStream();
				}
				if (transcodingStream == null)
					{
					this.TakeShot = false;
					}
				else
				{
				sourceVideo = transcodingStream.getSource().getVideo();
				}
				if (sourceVideo == null)
					{
					this.TakeShot = false;
					}

				if ( this.TakeShot == true )
					{
					sourceVideo.grabFrame(new CreateShot(this.StreamName,this.appInstance,this.Log), this.SnapShotHeight, this.SnapShotWidth);
					}

				this.TakeShot = true;

				synchronized(this)
					{
					try
						{
						Thread.currentThread().wait(this.SnapShotTimer);
						}
						catch (Exception e)
						{
						Log.info("WorkerClass.run: wait: "+e.toString());
						}
					}

				synchronized(this)
					{
					if (this.quit)
						{
						this.running = false;
						break;
						}
					}
			}
		}
	}
}
To enable the module, open the [install-dir]/conf/[application-name]/Application.xml file in a text editor and add the following <Module> to the <Modules> container in the file, it should be the last entry.
<Module><Name>SnapShot</Name>
	<Description>SnapShot</Description>
	<Class>com.wowza.demo.transcoder.timed.SnapShot</Class>
</Module>
The following properties can be added the last <Properties> container in [install-dir]/conf/[application]/Application.xml (be sure to get the correct <Properties> container, there are several in the Application.xml file).
<Property>
     <Name>streamName</Name>
     <Value>Stream1</Value>
</Property>
The StreamName property specifies the stream name to be used for snapshots. It defaults to myStream if not set.
 
<Property>
     <Name>snapHeight</Name>
     <Value>426</Value>
</Property>
The SnapHeight property specifies the height, in pixels, of the snapshot size. It defaults to 426 if not set.
 
<Property>
     <Name>snapWidth</Name>
     <Value>240</Value>
</Property>
The SnapWidth property specifies the width, in pixels, of the snapshot size. It defaults to 240 if not set.
 
<Property>
     <Name>snapTime</Name>
     <Value>500</Value>
</Property>
The SnapTime property specifies the time, in milliseconds, between snapshots. It defaults to 500 if not set.