Friday, 29 June 2012

HTML5 Video Pseudostreaming with Java 7

Adapted from the byte range request servlet code from the The BalusC Code, here is a Java 7 pseudostreaming servlet for playing video using the HTML5 video tag.

The servlet takes the name of a video file to play as a request parameter and the byte range of the video to stream in the Range header field.

PseudostreamingServlet.java

package org.adrianwalker.pseudostreaming;

import java.io.IOException;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.nio.file.StandardOpenOption.READ;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public final class PseudostreamingServlet extends HttpServlet {

  private static final int BUFFER_LENGTH = 1024 * 16;
  private static final long EXPIRE_TIME = 1000 * 60 * 60 * 24;
  private static final Pattern RANGE_PATTERN = Pattern.compile("bytes=(?<start>\\d*)-(?<end>\\d*)");
  private String videoPath;

  @Override
  public void init() throws ServletException {
    videoPath = getInitParameter("videoPath");
  }

  @Override
  protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
    processRequest(request, response);
  }

  private void processRequest(final HttpServletRequest request, final HttpServletResponse response) throws IOException {

    String videoFilename = URLDecoder.decode(request.getParameter("video"), "UTF-8");
    Path video = Paths.get(videoPath, videoFilename);

    int length = (int) Files.size(video);
    int start = 0;
    int end = length - 1;

    String range = request.getHeader("Range");
    Matcher matcher = RANGE_PATTERN.matcher(range);
    
    if (matcher.matches()) {
      String startGroup = matcher.group("start");
      start = startGroup.isEmpty() ? start : Integer.valueOf(startGroup);
      start = start < 0 ? 0 : start;

      String endGroup = matcher.group("end");
      end = endGroup.isEmpty() ? end : Integer.valueOf(endGroup);
      end = end > length - 1 ? length - 1 : end;
    }

    int contentLength = end - start + 1;

    response.reset();
    response.setBufferSize(BUFFER_LENGTH);
    response.setHeader("Content-Disposition", String.format("inline;filename=\"%s\"", videoFilename));
    response.setHeader("Accept-Ranges", "bytes");
    response.setDateHeader("Last-Modified", Files.getLastModifiedTime(video).toMillis());
    response.setDateHeader("Expires", System.currentTimeMillis() + EXPIRE_TIME);
    response.setContentType(Files.probeContentType(video));
    response.setHeader("Content-Range", String.format("bytes %s-%s/%s", start, end, length));
    response.setHeader("Content-Length", String.format("%s", contentLength));
    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

    int bytesRead;
    int bytesLeft = contentLength;
    ByteBuffer buffer = ByteBuffer.allocate(BUFFER_LENGTH);

    try (SeekableByteChannel input = Files.newByteChannel(video, READ);
            OutputStream output = response.getOutputStream()) {

      input.position(start);

      while ((bytesRead = input.read(buffer)) != -1 && bytesLeft > 0) {
        buffer.clear();
        output.write(buffer.array(), 0, bytesLeft < bytesRead ? bytesLeft : bytesRead);
        bytesLeft -= bytesRead;
      }
    }
  }
}

The location of the videos on the file system is configurable in the web.xml via the videoPath parameter for the stream servlet.

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <servlet>
        <servlet-name>stream</servlet-name>
        <servlet-class>org.adrianwalker.pseudostreaming.PseudostreamingServlet</servlet-class>
        <init-param>
            <param-name>videoPath</param-name>
            <param-value>/tmp/videos</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>stream</servlet-name>
        <url-pattern>/stream</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
</web-app>

Some simple HTML5 uses the servlet as the source for the video tag. The name of the video files is provided by the video URL parameter.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>HTML5 Video Pseudostreaming</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <div>
      <video id="video" controls>  
        <source src="stream?video=video.webm" />  
      </video>
    </div>
  </body>
</html>

You're going to need a video to play, so download a trailer or something:

http://trailers.apple.com/movies/sony_pictures/theamazingspiderman/amazingspiderman-tlr2b-USA_h480p.mov

The QuickTime trailer video file isn't supported by my target browser, so I need to convert it to another format, for this example WebM. ffmpeg can be used for the conversion:

ffmpeg -i amazingspiderman-tlr2b-USA_h480p.mov /tmp/videos/video.webm

Source Code

Usage

Run the project with 'mvn clean install jetty:run-war' and point your brower at http://localhost:8080/pseudostreaming.

C Legs

Every now and again I vow to learn C, and now seems like a good time to have another go.

My plan for learning C is always the same:

  1. Read K&R
  2. ?
  3. Profit

Where step 2 might as well be 'Don't use C and forget everything in K&R'. The problem is that I'm not a systems programmer, so everything I want to program is just easier to do in Java.

This time round I'm having a go with Learn C The Hard Way. It takes a way more modern and pragmatic approach than K&R, and introduces tools like make and valgrind early on.

I'm going to try and not fall back on more familiar languages, and see if I can knock out interesting things in C where appropriate, and blog about them to help me learn.

First off a simple Sudoku solver:

main.c

#include <stdio.h>
#include <stdlib.h>

#define _ 0
#define GRID_SIZE 9
#define SQUARE_SIZE 3
#define FALSE 0
#define TRUE !FALSE

void print(int grid[GRID_SIZE][GRID_SIZE]);
int complete(int grid[GRID_SIZE][GRID_SIZE]);
int contains(int array[GRID_SIZE], int value);
void row(int grid[GRID_SIZE][GRID_SIZE], int rowIndex, int row[GRID_SIZE]);
void column(int grid[GRID_SIZE][GRID_SIZE], int columnIndex, int column[GRID_SIZE]);
void square(int grid[GRID_SIZE][GRID_SIZE], int rowIndex, int columnIndex, int square[GRID_SIZE]);

int main(int argc, char** argv) {

  int grid[GRID_SIZE][GRID_SIZE] ={
    {_, 3, 6, _, 5, _, _, 8, 7},
    {9, _, 1, 7, 8, _, _, 6, 2},
    {_, _, 7, _, _, _, 1, 4, _},
    {_, _, 5, _, 9, _, _, _, 3},
    {_, _, _, _, 1, _, _, _, _},
    {8, _, _, _, 7, _, 6, _, _},
    {_, 2, 9, _, _, _, 5, _, _},
    {5, 7, _, _, 2, 9, 8, _, 6},
    {6, 1, _, _, 3, _, 2, 9, _}
  };

  int rowIndex;
  int columnIndex;
  int value;
  int possibleValue;
  int possibleValueCount;
  int inRow;
  int inColumn;
  int inSquare;
  int tmp[GRID_SIZE];

  while (!complete(grid)) {
    for (rowIndex = 0; rowIndex < GRID_SIZE; rowIndex++) {
      for (columnIndex = 0; columnIndex < GRID_SIZE; columnIndex++) {

        if (grid[rowIndex][columnIndex] != _) {
          continue;
        }

        possibleValue = _;
        possibleValueCount = 0;

        for (value = 1; value <= GRID_SIZE; value++) {

          row(grid, rowIndex, tmp);
          inRow = contains(tmp, value);
          if (inRow) {
            continue;
          }

          column(grid, columnIndex, tmp);
          inColumn = contains(tmp, value);
          if (inColumn) {
            continue;
          }

          square(grid, rowIndex, columnIndex, tmp);
          inSquare = contains(tmp, value);
          if (inSquare) {
            continue;
          }

          possibleValue = value;
          possibleValueCount++;
        }

        if (possibleValueCount == 1) {
          grid[rowIndex][columnIndex] = possibleValue;
        }
      }
    }
  }

  print(grid);

  return (EXIT_SUCCESS);
}

void print(int grid[GRID_SIZE][GRID_SIZE]) {

  int rowIndex;
  int columnIndex;

  for (rowIndex = 0; rowIndex < GRID_SIZE; rowIndex++) {
    for (columnIndex = 0; columnIndex < GRID_SIZE; columnIndex++) {
      if (columnIndex != 0) {
        printf(", ");
      }
      printf("%d", grid[rowIndex][columnIndex]);
    }
    printf("\n");
  }
}

int complete(int grid[GRID_SIZE][GRID_SIZE]) {
  int tmp[GRID_SIZE];
  int rowIndex;
  int inRow;

  for (rowIndex = 0; rowIndex < GRID_SIZE; rowIndex++) {
    row(grid, rowIndex, tmp);
    inRow = contains(tmp, _);
    if (inRow) {
      return FALSE;
    }
  }
  return TRUE;
}

int contains(int array[GRID_SIZE], int value) {

  int i;

  for (i = 0; i < GRID_SIZE; i++) {
    if (array[i] == value) {
      return TRUE;
    }
  }
  return FALSE;
}

void row(int grid[GRID_SIZE][GRID_SIZE], int rowIndex, int row[GRID_SIZE]) {

  int columnIndex;

  for (columnIndex = 0; columnIndex < GRID_SIZE; columnIndex++) {
    row[columnIndex] = grid[rowIndex][columnIndex];
  }
}

void column(int grid[GRID_SIZE][GRID_SIZE], int columnIndex, int column[GRID_SIZE]) {

  int rowIndex;

  for (rowIndex = 0; rowIndex < GRID_SIZE; rowIndex++) {
    column[rowIndex] = grid[rowIndex][columnIndex];
  }
}

void square(int grid[GRID_SIZE][GRID_SIZE], int rowIndex, int columnIndex, int square[GRID_SIZE]) {

  int i;
  int j;

  int x = SQUARE_SIZE * (rowIndex / SQUARE_SIZE);
  int y = SQUARE_SIZE * (columnIndex / SQUARE_SIZE);

  for (i = 0; i < SQUARE_SIZE; i++) {
    for (j = 0; j < SQUARE_SIZE; j++) {
      square[(i * SQUARE_SIZE) + j] = grid[x + i][y + j];
    }
  }
}