Saturday, May 22, 2010

Get first and last thumb of a video using Java and FFMpeg

In the post Java Video to FLV (Flash Video) converter using FFmpeg we saw how almost any video format can be easily converted to FLV video file using FFMpeg.

FFMpeg can do much more. It can also grab a thumb at any size from any point of time in the video. We will see how it can be used to get the first and the last thumbs of a video. Getting the first thumb of a video is not so hard. You simply have to take the thumb at zero or first second of the video.

In order to achieve that, we will first construct a Java class that can take a thumb from a given video file for a given second minute and hour:"

package com.bashan.blog.video;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
/**
 * @author Bashan
 */
public class VideoThumbTaker
{
  protected String ffmpegApp;
  public VideoThumbTaker(String ffmpegApp)
  {
    this.ffmpegApp = ffmpegApp;
  }
  public void getThumb(String videoFilename, String thumbFilename, int width, int height,int hour, int min, float sec)
      throws IOException, InterruptedException
  {
    ProcessBuilder processBuilder = new ProcessBuilder(ffmpegApp, "-y", "-i", videoFilename, "-vframes", "1",
        "-ss", hour + ":" + min + ":" + sec, "-f", "mjpeg", "-s", width + "*" + height, "-an", thumbFilename);
    Process process = processBuilder.start();
    InputStream stderr = process.getErrorStream();
    InputStreamReader isr = new InputStreamReader(stderr);
    BufferedReader br = new BufferedReader(isr);
    String line;
    while ((line = br.readLine()) != null);
    process.waitFor();
  }
  public static void main(String[] args)
  {
    VideoThumbTaker videoThumbTaker = new VideoThumbTaker("C:\\ffmpeg.exe");
    try
    {
      videoThumbTaker.getThumb("C:\\someVideo.flv", "C:\\thumbTest.png", 120, 100, 0, 0, 10);
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
  }
}


Take a look at the test program, showing how a thumb is being taken from the 10th second of a video named: “someVideo.flv”. Note that we pass to the constructor the class the location of your FFMpeg tool. Also note, that we pass the width and height of the thumb.



After we have a class that knows to grab the thumb of any size from any video on a specific point in time, we can easily construct a class that grabs the first thumb of a video:



package com.bashan.blog.video;
import java.io.IOException;
/**
 * @author Bashan
 */
public class VideoFirstThumbTaker extends VideoThumbTaker
{
  public VideoFirstThumbTaker(String ffmpegApp)
  {
    super(ffmpegApp);
  }
  public void getThumb(String videoFilename, String thumbFilename, int width, int height)
      throws IOException, InterruptedException
  {
    super.getThumb(videoFilename, thumbFilename, width, height, 0, 0, 1);
  }
}


In that class we simply extend our “VideoThumbTaker” to grab the first second of the video. Grabbing the zero second of the video is not a good idea, since the first frame in many videos usually black.



In order to get the last frame of a video, we first have to know what is it’s length in hours, minutes and seconds. FFMpeg can also help us with that. We will take a dummy thumb from the video, by doing it, FFMpeg will write to the standard output the video information. We will use simple regular expression to extract the length of the video in term of hours, minutes and second. and finally, we will delete the dummy thumb that was created:



package com.bashan.blog.video;
import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * @author Bashan
 */
public class VideoInfo {
  private String ffmpegApp;
  private int hours;
  private int minutes;
  private float seconds;
  public VideoInfo(String ffmpegApp) {
    this.ffmpegApp = ffmpegApp;
  }
  public void getInfo(String videoFilename)
      throws IOException, InterruptedException {
    String tmpFile = videoFilename + ".tmp.png";
    ProcessBuilder processBuilder = new ProcessBuilder(ffmpegApp, "-y", "-i", videoFilename, "-vframes", "1",
        "-ss", "0:0:0", "-an", "-vcodec", "png", "-f", "rawvideo", "-s", "100*100", tmpFile);
    Process process = processBuilder.start();
    InputStream stderr = process.getErrorStream();
    InputStreamReader isr = new InputStreamReader(stderr);
    BufferedReader br = new BufferedReader(isr);
    String line;
    StringBuffer sb = new StringBuffer();
    while ((line = br.readLine()) != null) {
      sb.append(line);
    }
    new File(tmpFile).delete();
    Pattern pattern = Pattern.compile("Duration: (.*?),");
    Matcher matcher = pattern.matcher(sb);
    if (matcher.find()) {
      String time = matcher.group(1);
      calcTime(time);
    }
    process.waitFor();
  }
  private void calcTime(String timeStr) {
    String[] parts = timeStr.split(":");
    hours = Integer.parseInt(parts[0]);
    minutes = Integer.parseInt(parts[1]);
    seconds = Float.parseFloat(parts[2]);
  }
  public int getHours() {
    return hours;
  }
  public int getMinutes() {
    return minutes;
  }
  public float getSeconds() {
    return seconds;
  }
  public static void main(String[] args) {
    VideoInfo videoInfo = new VideoInfo("C:\\ffmpeg.exe");
    try {
      videoInfo.getInfo("C:\\testVideo.wmv");
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}


We will now use our “VideoInfo” class to construct: “VideoLastThumbTaker”. We will take the duration of the video, and subtract from it’s seconds part a small value of: 0.2 second , in order to take almost the last thumb of the video. Note: This class doesn’t handle properly a case in which the video seconds part length is a bit less than 0.2 seconds. I assume you won’t have much terrible handling this case yourself. Let’s have a look at the class:



package com.bashan.blog.video;
import java.io.IOException;
/**
 * @author Bashan
 */
public class VideoLastThumbTaker extends VideoThumbTaker
{
  public VideoLastThumbTaker(String ffmpegApp)
  {
    super(ffmpegApp);
  }
  public void getThumb(String videoFilename, String thumbFilename, int width, int height)
      throws IOException, InterruptedException
  {
    VideoInfo videoInfo = new VideoInfo(ffmpegApp);
    videoInfo.getInfo(videoFilename);
    super.getThumb(videoFilename, thumbFilename, width, height, videoInfo.getHours(),
                                                                videoInfo.getMinutes(), videoInfo.getSeconds() - 0.2f);
  }
}

You can download the source of that classes here.

Tuesday, May 4, 2010

Tomcat Performance: gzip Compression

One of the ways of increasing server performance, is by reducing the amount of data passed on the network. This can be done by using compression.
Most modern web browsers support compression. If a browser supports compression it will send in the request header the following string:
Accept-Encoding: gzip,deflate
The server can identify this header and return a compressed response. If the browser doesn’t send this header, the server will always return an uncompressed response. If the response is compressed, it will return in the response header the following string:
Content-Encoding: gzip
Now the client knows that the response is gzip compressed.
Tomcat supports gzip compression out of the box. You can easily add compression support to your server, without writing a single line of code.
To turn on Tomcat compression, you should edit server.xml, which is located under Tomcat conf directory. The compression is added to the connector element:
<Connector port="8080" protocol="HTTP/1.1" 
connectionTimeout="20000" redirectPort="8443" 
compression="on" compressableMimeType="text/html,
text/xml,text/plain,text/javascript,text/css" 
/>
Note that you can determine the mime types for which tomcat should do the compression.
Sometimes there are user agents (web browsers) that has problems with gzip compression, even If they send Content-Encoding: gzip direction in the request header. In order to exclude specific user agents from being served with gzip compression you can use the property: noCompressionUserAgents. You can use regular expression to define user agents list separated with commas:
<Connector port="8080" protocol="HTTP/1.1" 
connectionTimeout="20000" redirectPort="8443" 
compression="on" compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css" 
noCompressionUserAgents=".*MSIE 6.*"
/>


If a file is too small, the overhead of compressing it may take longer than sending it uncompressed. You can define a minimum size for which a compression should not be done:
<Connector port="8080" protocol="HTTP/1.1" 
connectionTimeout="20000" redirectPort="8443"
compression="on" compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css" 
noCompressionUserAgents=".*MSIE 6.*"
compressionMinSize="1024"
/>

Saturday, May 1, 2010

Installing Apache FLV module on Windows

Most modern FLV players can show a video from a specific point. But this can only be done with server side help. In order to support this feature you will first have to inject meta data to the FLV file. This can be done, for example, with a command line named: Yamdi

The meta data, is actually a vector mapping specific points in time to their corresponding byte in the FLV file. When you move the video progress bar, the player is sending a request for the FLV file with a parameter named “start”. The parameter contains the specific byte in the FLV file from which we would like to see the vide.

Apache has a very nice module, named mod_flvx, that supports FLV streaming out of the box. When it receives a request for FLV with with the “start” parameter, it automatically knows to return the file starting at the given byte.

Finding a Windows compiled version of the module took me quite some time. So I put it here as a direct download.

Put the module under Apache modules directory and add these lines in your httpd.conf configuration file:

LoadModule flvx_module modules/mod_flvx.so
AddHandler flv-stream .flv

Restart your server, and you are ready to go. You can test the module, but making a request with the start parameter to some FLV file. For example:

http://localhost/myflv.flv?start=10000

If the module is installed properly, Apache will return the FLV file starting from byte 10,000.