--- /dev/null
+/*\r
+ * Copyright © 2010-2011 Amichai Rothman\r
+ *\r
+ * This file is part of JFLVStream - the Java FLV Pseudostreaming package.\r
+ *\r
+ * JFLVStream is free software: you can redistribute it and/or modify\r
+ * it under the terms of the GNU General Public License as published by\r
+ * the Free Software Foundation, either version 3 of the License, or\r
+ * (at your option) any later version.\r
+ *\r
+ * JFLVStream is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+ * GNU General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU General Public License\r
+ * along with JFLVStream. If not, see <http://www.gnu.org/licenses/>.\r
+ *\r
+ * For additional info see http://www.freeutils.net/source/jflvstream/\r
+ */\r
+\r
+package com.pentila.entSavoie.servlet;\r
+\r
+import java.io.*;\r
+import javax.servlet.http.HttpServletRequest;\r
+import javax.servlet.http.HttpServletResponse;\r
+\r
+/**\r
+ * The {@code FLVPseudoStreamer} class provides a server-side FLV pseudostreaming implementation.\r
+ * <p>\r
+ * An FLV stream consists of a header, followed by a series of tag blocks.\r
+ * A description of the format can be found <a href="http://osflash.org/flv">here</a>,\r
+ * however it requires clarification: each block begins with an 11-byte header, followed\r
+ * by the block data, followed by a 4-byte total block size value (the data size,\r
+ * as it appears in the 3-byte header field, plus 11). The FLV header itself is reminiscent\r
+ * of the tag blocks, as it also contains a header size field (although it's 4 bytes and at\r
+ * a different offset than within tags), and followed by a 4-byte total block size value\r
+ * (although for the header, it's always zero).\r
+ * <p>\r
+ * The pseudostreaming works by having the client ask for a specific start position\r
+ * within the FLV stream, which corresponds to the first byte of a tag containing a key\r
+ * frame. The server then sends an FLV stream header followed by the data starting at this\r
+ * position, which effectively looks to the client like a new, valid, FLV stream which\r
+ * contains the video starting at the requested key frame.\r
+ * <p>\r
+ * See the {@link #service service} method for a usage example.\r
+ * \r
+ * @author Amichai Rothman\r
+ * @since 2010-06-23\r
+ */\r
+public class FLVPseudoStreamer {\r
+\r
+ protected static final int HEADER_LENGTH = 13;\r
+ protected static final byte[] SIGNATURE = { 'F', 'L', 'V', 0x01 };\r
+ \r
+ protected File basedir;\r
+ protected InputStream flvStream;\r
+ protected String filename;\r
+ protected long length;\r
+ \r
+ protected boolean allowCaching;\r
+ protected boolean allowDynamicBandwidthLimit;\r
+ protected int packetInterval;\r
+ protected int packetSize;\r
+ \r
+ /**\r
+ * Constructs a streamer which allows dynamic bandwidth limiting.\r
+ */\r
+ public FLVPseudoStreamer() {\r
+ setAllowDynamicBandwidthLimit(true);\r
+ setBandwidthLimit(-1, -1);\r
+ }\r
+ \r
+ /**\r
+ * Sets the base directory where the files to be streamed reside.\r
+ * \r
+ * @param dir the base directory\r
+ * @throws IOException if the given directory is invalid\r
+ */\r
+ public void setBaseDirectory(String dir) throws IOException {\r
+ File basedir = new File(dir);\r
+ if (!basedir.isDirectory())\r
+ throw new IOException("directory not found: " + dir);\r
+ this.basedir = basedir.getCanonicalFile();\r
+ }\r
+ \r
+ /**\r
+ * Sets the filename of the file to stream, relative\r
+ * to the {@link #setBaseDirectory base directory}.\r
+ * \r
+ * @param filename the filename of the file to stream, relative to\r
+ * the base directory\r
+ * @throws IOException if the filename is invalid, the file cannot\r
+ * be read or it is not under the base directory\r
+ */\r
+ public void setFLVFile(String filename) throws IOException {\r
+ if (filename == null)\r
+ throw new IOException("missing file name");\r
+ File file = new File(basedir, filename);\r
+ if (!file.canRead())\r
+ throw new IOException("can't read file: " + filename);\r
+ file = file.getCanonicalFile();\r
+ if (!file.getCanonicalPath().startsWith(basedir.getCanonicalPath()))\r
+ throw new IOException("access denied");\r
+ setFLVStream(new BufferedInputStream(\r
+ new FileInputStream(file)), file.getName(), file.length());\r
+ }\r
+ \r
+ /**\r
+ * Sets the data for the FLV stream.\r
+ * \r
+ * @param flvStream the full FLV data stream\r
+ * @param filename the FLV file name (only the base name, without path)\r
+ * @param length the total FLV stream length\r
+ */\r
+ public void setFLVStream(InputStream flvStream, String filename, long length) {\r
+ this.flvStream = flvStream;\r
+ this.filename = filename;\r
+ this.length = length;\r
+ }\r
+ \r
+ /**\r
+ * Sets the bandwidth limit for the streamed data. The limit is actually an average,\r
+ * defined in terms of packet size and time interval: after a packet of the\r
+ * given size is sent (at maximum burst speed), the next packet will not be sent\r
+ * until the interval (starting at the beginning of the packet) has passed.\r
+ * <p>\r
+ * The effective average limit in kilobytes per second is thus<br>\r
+ * {@code (packetSize / 1024 ) / (packetInterval / 1000)}<br>\r
+ * or<br>\r
+ * {@code 1000 * (packetSize / 1024) / packetInterval}<br>\r
+ * or<br>\r
+ * {@code 0.9765625 * packetSize / packetInterval}.\r
+ * <p>\r
+ * If either of the parameters is set to a non-positive value,\r
+ * the bandwidth is effectively unlimited.\r
+ * \r
+ * @param packetInterval the interval between beginnings of packets in milliseconds\r
+ * @param packetSize the packet size in bytes\r
+ */\r
+ public void setBandwidthLimit(int packetInterval, int packetSize) {\r
+ if (packetInterval < 1 || packetSize < 1) {\r
+ packetInterval = 1000;\r
+ packetSize = Integer.MAX_VALUE; // effectively unlimited\r
+ }\r
+ this.packetInterval = packetInterval;\r
+ this.packetSize = packetSize;\r
+ }\r
+ \r
+ /**\r
+ * Sets the bandwidth limits for the streamed data.\r
+ * <p>\r
+ * The following are the valid values, and their interpretation:\r
+ * <table border="1">\r
+ * <tr><th>Value</th><th>Packet Size (b)</th><th>Packet Interval (ms)</th><th>Effective limit (kb/s)</tr>\r
+ * <tr><td>Any integer {@code n}</td><td>{@code n} * 256</td><td>250</td><td>{@code n}</td></tr>\r
+ * <tr><td>"low"</td><td>10 * 1024</td><td>1000</td><td>10</td></tr>\r
+ * <tr><td>"mid"</td><td>40 * 1024</td><td>500</td><td>80</td></tr>\r
+ * <tr><td>"high"</td><td>90 * 1024</td><td>300</td><td>300</td></tr>\r
+ * <tr><td>Anything else</td><td>90 * 1024</td><td>300</td><td>300</td></tr>\r
+ * </table>\r
+ * \r
+ * @param limit the bandwidth limit\r
+ */\r
+ public void setBandwidthLimit(String limit) {\r
+ int interval;\r
+ int size;\r
+ if ("low".equals(limit)) {\r
+ interval = 1000;\r
+ size = 10 * 1024;\r
+ } else if ("mid".equals(limit)) {\r
+ interval = 500;\r
+ size = 40 * 1024;\r
+ } else if ("high".equals(limit)) {\r
+ interval = 300;\r
+ size = 90 * 1024;\r
+ } else {\r
+ try {\r
+ int kbps = Integer.parseInt(limit);\r
+ interval = 1000 / 4;\r
+ size = kbps * 1024 / 4;\r
+ } catch (NumberFormatException nfe) {\r
+ interval = 300;\r
+ size = 90 * 1024;\r
+ }\r
+ }\r
+ setBandwidthLimit(interval, size);\r
+ }\r
+ \r
+ /**\r
+ * Sets whether the client is allowed to request a specified bandwidth limit.\r
+ * \r
+ * @param allow true if dynamic bandwidth limit is allowed, false otherwise\r
+ */\r
+ public void setAllowDynamicBandwidthLimit(boolean allow) {\r
+ this.allowDynamicBandwidthLimit = allow;\r
+ }\r
+ \r
+ /**\r
+ * Sets whether client caching of the streamed FLV data is allowed. If not,\r
+ * the appropriate HTTP headers are sent to prevent caching.\r
+ * \r
+ * @param allow true if caching is allowed, false otherwise\r
+ */\r
+ public void setAllowCaching(boolean allow) {\r
+ this.allowCaching = allow;\r
+ }\r
+\r
+ /**\r
+ * Returns the full length of the stream (in bytes).\r
+ * \r
+ * @return the full length of the stream (in bytes)\r
+ */\r
+ public long length() {\r
+ return length;\r
+ }\r
+ \r
+ /**\r
+ * Returns the length of the streamed data when starting at the given\r
+ * position in the stream (in bytes). This includes the header that will\r
+ * be prepended to the data if necessary, and the data itself.\r
+ * \r
+ * @param pos the position within the file from which the data\r
+ * will be streamed. If non-positive, the full length of the\r
+ * file is returned.\r
+ * @return the length of the streamed data when starting at the given position\r
+ */\r
+ public long length(long pos) {\r
+ return pos <= 0 ? length : HEADER_LENGTH + length - pos;\r
+ }\r
+ \r
+ /**\r
+ * Reads the header from the given FLV stream.\r
+ * \r
+ * @param in an FLV stream\r
+ * @return the FLV header\r
+ * @throws IOException if an I/O error occurs, or if the\r
+ * given stream does not begin with a valid FLV header\r
+ */\r
+ protected byte[] readHeader(InputStream in) throws IOException {\r
+ byte[] header = new byte[HEADER_LENGTH];\r
+ // read header\r
+ int len = HEADER_LENGTH;\r
+ while (len > 0)\r
+ len -= in.read(header, HEADER_LENGTH - len, len);\r
+ // validate it\r
+ for (int i = 0; i < SIGNATURE.length; i++)\r
+ if (header[i] != SIGNATURE[i])\r
+ throw new IOException("invalid FLV signature");\r
+ return header;\r
+ }\r
+ \r
+ /**\r
+ * Writes a valid FLV stream to the given output stream, with the data starting\r
+ * at the given position in the FLV stream. Both streams are closed by this method.\r
+ * \r
+ * @param out the stream to write to\r
+ * @param pos the position within the FLV stream where the requested data starts\r
+ * (this should be either the first byte of the data, or the first byte of\r
+ * a tag containing a key frame)\r
+ * @throws IOException if an error occurs or the data stream does not begin with a\r
+ * valid FLV header\r
+ * @throws InterruptedIOException if transfer is interrupted in the middle, e.g. when\r
+ * the client seeks to a different position in the stream\r
+ */\r
+ public void stream(OutputStream out, long pos) throws IOException, InterruptedIOException {\r
+ if (pos <= 0)\r
+ pos = HEADER_LENGTH; // data starts at first tag after header\r
+ long len = length - pos;\r
+ byte[] buf = new byte[16384];\r
+\r
+ try {\r
+ // write FLV header\r
+ byte[] header = readHeader(flvStream);\r
+ out.write(header, 0, HEADER_LENGTH);\r
+ // skip to start position\r
+ pos -= HEADER_LENGTH; // header has already been read from stream\r
+ while (pos > 0)\r
+ pos -= flvStream.skip(pos);\r
+ // stream the data\r
+ while (len > 0) {\r
+ // write full packet\r
+ long packetStartTime = System.currentTimeMillis();\r
+ int remaining = (int)Math.min(len, packetSize);\r
+ int count = 0;\r
+ while ((count = flvStream.read(buf, 0, Math.min(buf.length, remaining))) > 0) {\r
+ try {\r
+ out.write(buf, 0, count);\r
+ } catch (IOException ioe) {\r
+ // when client seeks to new position within stream, the previous stream is\r
+ // broken, so we treat it as a special interrupt and not regular IOException\r
+ throw new InterruptedException("streaming interrupted");\r
+ }\r
+ remaining -= count;\r
+ len -= count;\r
+ }\r
+ long remainingTime = packetStartTime + packetInterval - System.currentTimeMillis();\r
+ if (remainingTime > 0)\r
+ Thread.sleep(remainingTime);\r
+ }\r
+ } catch (InterruptedException ie) {\r
+ InterruptedIOException iioe = new InterruptedIOException("streaming interrupted");\r
+ iioe.bytesTransferred = (int)(length - len);\r
+ throw iioe;\r
+ } finally {\r
+ try {\r
+ flvStream.close();\r
+ } catch (IOException ignore) {}\r
+ try {\r
+ out.close();\r
+ } catch (IOException ignore) {\r
+ // client may close the stream when data ends, causing a broken pipe exception\r
+ // so we just ignore it\r
+ }\r
+ }\r
+ }\r
+ \r
+ /**\r
+ * Sets headers in the given response to prevent it from being cached.\r
+ * \r
+ * @param response the response which should not be cached\r
+ */\r
+ public static void preventCaching(HttpServletResponse response) {\r
+ response.setHeader("Pragma", "no-cache");\r
+ response.setHeader("Cache-Control", "no-cache");\r
+ response.addHeader("Cache-Control", "no-store");\r
+ response.addHeader("Cache-Control", "private");\r
+ response.addHeader("Cache-Control", "max-stale=0");\r
+ response.addHeader("Cache-Control", "max-age=0");\r
+ response.setDateHeader("Expires", 0);\r
+ }\r
+ \r
+ /**\r
+ * Serves a request to stream an FLV file.\r
+ * <p>\r
+ * This method can either be called directly from a servlet, or just viewed as an\r
+ * example of how this class might be used by a servlet or other communications\r
+ * framework (the rest of this class is independent of the Servlet API).\r
+ * <p>\r
+ * The supported request parameters are:\r
+ * <ul>\r
+ * <li>{@code file} - the name of the file to stream, relative to the base directory.\r
+ * <li>{@code start} - the position within the FLV file where the data starts (this\r
+ * should be either the first byte of the file, or the first byte of a tag\r
+ * containing a key frame). If omitted, the file is streamed from its beginning.\r
+ * <li>{@code bw} - a bandwidth limit string, passed to the\r
+ * {@link #setBandwidthLimit(String) setBandwidthLimit} method. If dynamic bandwidth\r
+ * is not allowed or this parameter is omitted, the previously set limit is used.\r
+ * </ul>\r
+ * <p>\r
+ * The HTTP response headers are set as follows:\r
+ * <ul>\r
+ * <li>Content-Type is set to "video/x-flv".\r
+ * <li>Content-Disposition is set to "attachment" with the original filename.\r
+ * <li>Content-Length is set to the appropriate (full or partial) stream length.\r
+ * <li>If caching is not allowed, the appropriate HTTP headers are set to prevent caching.\r
+ * <li>If an error occurs, an appropriate HTTP response code is sent.\r
+ * \r
+ * @param request the request containing the streaming parameters\r
+ * @param response the response to which the file is streamed\r
+ * @throws IOException if an error occurs\r
+ * @throws InterruptedIOException if transfer is interrupted in the middle,\r
+ * e.g. when the client seeks to a different position in the streams\r
+ */\r
+ public void service(HttpServletRequest request, HttpServletResponse response)\r
+ throws IOException, InterruptedIOException {\r
+ try {\r
+ // parse parameters\r
+ String filename = request.getParameter("file");\r
+ setFLVFile(filename);\r
+ if (allowDynamicBandwidthLimit)\r
+ setBandwidthLimit(request.getParameter("bw"));\r
+ String ppos = request.getParameter("start");\r
+ long pos = ppos == null || ppos.length() == 0 ? 0 : Long.parseLong(ppos);\r
+ // prepare response headers\r
+ response.setStatus(HttpServletResponse.SC_OK);\r
+ response.setContentType("video/x-flv");\r
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + this.filename + "\"");\r
+ response.setHeader("Content-Length", Long.toString(length(pos))); // setContentLength() is limited to 2GB\r
+ if (!allowCaching)\r
+ preventCaching(response);\r
+ stream(response.getOutputStream(), pos);\r
+ } catch (IllegalArgumentException iae) {\r
+ iae.printStackTrace();\r
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, iae.getMessage());\r
+ } catch (InterruptedIOException iioe) {\r
+ // this is ok, client likely seeked to new position within stream\r
+ } catch (IOException ioe) {\r
+ ioe.printStackTrace();\r
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error streaming file");\r
+ } catch (Throwable t) {\r
+ t.printStackTrace();\r
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error processing request");\r
+ }\r
+ }\r
+ \r
+ public void service(HttpServletRequest request,\r
+ HttpServletResponse response,\r
+ String fileName,\r
+ InputStream is,\r
+ int contentLength,\r
+ String contentType)\r
+ throws IOException, InterruptedIOException {\r
+ try {\r
+ setFLVStream(new BufferedInputStream(is), fileName, contentLength);\r
+ \r
+ if (allowDynamicBandwidthLimit)\r
+ setBandwidthLimit(request.getParameter("bw"));\r
+ String ppos = request.getParameter("start");\r
+ long pos = ppos == null || ppos.length() == 0 ? 0 : Long.parseLong(ppos);\r
+ // prepare response headers\r
+ response.setStatus(HttpServletResponse.SC_OK);\r
+ response.setContentType("video/x-flv");\r
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + this.filename + "\"");\r
+ response.setHeader("Content-Length", Long.toString(length(pos))); // setContentLength() is limited to 2GB\r
+ if (!allowCaching)\r
+ preventCaching(response);\r
+ stream(response.getOutputStream(), pos);\r
+ } catch (IllegalArgumentException iae) {\r
+ iae.printStackTrace();\r
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, iae.getMessage());\r
+ } catch (InterruptedIOException iioe) {\r
+ // this is ok, client likely seeked to new position within stream\r
+ } catch (IOException ioe) {\r
+ ioe.printStackTrace();\r
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error streaming file");\r
+ } catch (Throwable t) {\r
+ t.printStackTrace();\r
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error processing request");\r
+ }\r
+}\r
+ \r
+}\r