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:
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
- GitHub - pseudostreaming
Pseudo-streaming Maven Project - pseudostreaming.zip
Usage
Run the project with 'mvn clean install jetty:run-war' and point your brower at http://localhost:8080/pseudostreaming.