diff --git a/src/main/java/sagex/UIContext.java b/src/main/java/sagex/UIContext.java index 58abdd3..42582a4 100644 --- a/src/main/java/sagex/UIContext.java +++ b/src/main/java/sagex/UIContext.java @@ -40,7 +40,7 @@ public static UIContext getCurrentContext() { // still no context, try the thread parse hack if (ctx == null) { String name = Thread.currentThread().getName(); - log.debug("Attempting to parse UIContext from thread name: " + name); + // log.debug("Attempting to parse UIContext from thread name: " + name); // try to parse the context from the thread Matcher m = threadPattern.matcher(name); if (m.find()) { @@ -55,7 +55,7 @@ public static UIContext getCurrentContext() { } if (ctx != null) { - log.debug("Discovered UIContext: " + ctx); + // log.debug("Discovered UIContext: " + ctx); ui = new UIContext(ctx); setCurrentContext(ui); } else { @@ -68,7 +68,7 @@ public static UIContext getCurrentContext() { } if (ui==null) { - log.debug("No Context was found!"); + // log.debug("No Context was found!"); } } diff --git a/src/main/java/sagex/remote/SagexServlet.java b/src/main/java/sagex/remote/SagexServlet.java index 1ecf75e..a041b4f 100644 --- a/src/main/java/sagex/remote/SagexServlet.java +++ b/src/main/java/sagex/remote/SagexServlet.java @@ -19,6 +19,8 @@ import sagex.util.LogProvider; public class SagexServlet extends HttpServlet { + public static final String DEBUG_ATTRIBUTE = "SagexServlet.debug"; + private static final long serialVersionUID = 1L; public interface SageHandler { @@ -41,14 +43,18 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - log.debug("Handling remote request: " + req.getPathInfo()); + log.debug("Handling remote request: " + req.getPathInfo() + (req.getQueryString()!=null?("?" + req.getQueryString()):"")); + if ("true".equals(req.getParameter("_debug")) || req.getPathInfo().contains(".debug.")) { + req.setAttribute(DEBUG_ATTRIBUTE, true); + } + try { try { - if ("true".equals(req.getParameter("_debug"))) { - log.debug("BEGIN DEBUG: " + req.getPathInfo()); + if (req.getAttribute(DEBUG_ATTRIBUTE)!=null) { + log.debug("BEGIN DEBUG: " + req.getPathInfo() + "?" + req.getQueryString()); for (Enumeration e = req.getHeaderNames(); e.hasMoreElements();) { String n = e.nextElement(); - log.debug(String.format("HEADER: %s=%s\n", n, req.getHeader(n))); + log.debug(String.format("REQ HEADER: %s=%s\n", n, req.getHeader(n))); } log.debug("END DEBUG"); } @@ -81,7 +87,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S sh.handleRequest(args, req, resp); } catch (Throwable t) { log.warn("Failed to process Sage Handler!", t); - resp.sendError(500, "Sage Servlet Failed: " + t.getMessage()); + if (!resp.isCommitted()) { + resp.sendError(500, "Sage Servlet Failed: " + t.getMessage()); + } } } diff --git a/src/main/java/sagex/remote/media/MediaFileRequestHandler.java b/src/main/java/sagex/remote/media/MediaFileRequestHandler.java index c70dd7b..4ea27f9 100644 --- a/src/main/java/sagex/remote/media/MediaFileRequestHandler.java +++ b/src/main/java/sagex/remote/media/MediaFileRequestHandler.java @@ -9,6 +9,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; +import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.RandomAccess; import javax.servlet.http.HttpServletRequest; @@ -16,12 +18,32 @@ import sagex.api.AiringAPI; import sagex.api.MediaFileAPI; +import sagex.remote.SagexServlet; import sagex.util.ILog; import sagex.util.LogProvider; +//Status Code: 206 +//Header: null=HTTP/1.1 206 (Partial Content) +//Header: Accept-Ranges=bytes +//Header: Connection=close +//Header: Content-Length=250000 +//Header: Content-Range=bytes 5076471732-5076721731/5076721732 +//Header: Content-Type=application/octet-stream +//Header: Date=Sat, 18 Jan 2014 22:20:05 GMT +//Header: ETag="6ead47e148f2a26eef9318f6cb9801e70d5340b79.ts" +//Header: Last-Modified=Sun, 30 Sep 2012 10:25:40 GMT +//Header: Server=nginx/1.2.5 + public class MediaFileRequestHandler implements SageMediaRequestHandler { ILog log = LogProvider.getLogger(this.getClass()); + public String formatDate(long time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(time); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); + return dateFormat.format(calendar.getTime()); + } + public void processRequest(HttpServletRequest req, HttpServletResponse resp, Object sagefile) throws Exception { if (req.getAttribute(MediaHandler.MEDIA_M3U_ATTRIBUTE)!=null) { processM3U(req, resp, sagefile); @@ -32,8 +54,14 @@ public void processRequest(HttpServletRequest req, HttpServletResponse resp, Obj // get the media file that we are going to be using File file = MediaFileAPI.GetFileForSegment(sagefile, getMediaSegment(req)); - if (!file.exists()) - throw new FileNotFoundException(file.getAbsolutePath()); + if (file == null || !file.exists()) { + // TODO: This happens because it's looking for + // subtitle files... need to handle subtitles at some point. + + log.info("MediaFile not found for " + req.getPathInfo()); + resp.sendError(404, "File not found"); + return; + } // check for a range request String range = req.getHeader("Range"); @@ -76,6 +104,8 @@ private void processM3U(HttpServletRequest req, HttpServletResponse resp, // http://example.com/movie1/fileSequenceD.ts // #EXT-X-ENDLIST + int mfid = MediaFileAPI.GetMediaFileID(sagefile); + StringBuilder sb = new StringBuilder(); sb.append("#EXTM3U\n"); sb.append("#EXT-X-PLAYLIST-TYPE:VOD\n"); @@ -83,16 +113,59 @@ private void processM3U(HttpServletRequest req, HttpServletResponse resp, sb.append("#EXT-X-VERSION:3\n"); sb.append("#EXT-X-MEDIA-SEQUENCE:0\n"); for (int i=0;i= 0) { + return file.getName().substring(i+1); + } else { + return ""; + } + } private long getDurationInSeconds(Object sagefile, int i) { return MediaFileAPI.GetDurationForSegment(sagefile, i)/1000; @@ -104,39 +177,53 @@ private long getDurationInSeconds(Object sagefile) { public void doRangeRequest(File file, HttpServletRequest req, HttpServletResponse resp, Object sagefile, long[] ranges) throws IOException { - - // TODO: Set Content length to be the length of the range - // Content-Range: bytes 21010-47021/47022 - // and set the content range header - // if we can't satisfy the range, then send back 416 code - - resp.setHeader("Accept-Ranges", "bytes"); + doRangeRequestWithStatus(file, req, resp, sagefile, ranges); + } + public void setHeader(HttpServletResponse resp, String header, String value, HttpServletRequest req) { + resp.addHeader(header, value); + if (req.getAttribute(SagexServlet.DEBUG_ATTRIBUTE)!=null) { + log.debug("RESP HEADER: " + header + "=" + value); + } + } + + public void doRangeRequestWithStatus(File file, HttpServletRequest req, HttpServletResponse resp, Object sagefile, long[] ranges) + throws IOException { + + setHeader(resp, "Accept-Ranges", "bytes", req); + setHeader(resp, "Connection", "close", req); + setHeader(resp, "Date", formatDate(System.currentTimeMillis()), req); + setHeader(resp, "Last-Modified", formatDate(file.lastModified()), req); + if (MediaFileAPI.IsMusicFile(sagefile)) { - resp.setContentType("audio/mpeg"); + setHeader(resp, "Content-Type","audio/mpeg", req); } else if (MediaFileAPI.IsPictureFile(sagefile)) { - resp.setContentType("image/jpeg"); + setHeader(resp, "Content-Type","image/jpeg", req); } else { - resp.setContentType("video/mpeg"); + setHeader(resp, "Content-Type","application/octet-stream", req); } String forceMime = req.getParameter("force-mime"); if (forceMime != null && forceMime.length() > 0) { - resp.setContentType(forceMime); + setHeader(resp, "Content-Type", forceMime, req); } RandomAccessFile raf = new RandomAccessFile(file, "r"); try { - // as per spec, send 206 for ranges - resp.setStatus(206); + if (ranges==null) { + resp.setStatus(200); + ranges = new long[] {0, file.length()-1}; + } else { + resp.setStatus(206); + } raf.seek(ranges[0]); FileInputStream fis = new FileInputStream(raf.getFD()); - long size = ranges[1] - ranges[0]; - resp.setHeader("Content-Length", String.valueOf(size)); + long size = ranges[1] - ranges[0] + 1; + setHeader(resp, "Content-Length", String.valueOf(size), req); - String contentRange = "bytes "+String.valueOf(ranges[0])+"-"+String.valueOf(ranges[1])+"/"+String.valueOf(file.length()); - resp.setHeader("Content-Range", contentRange); + String contentRange = "bytes "+String.valueOf(ranges[0])+"-"+String.valueOf((ranges[1]))+"/"+String.valueOf(file.length()); + setHeader(resp, "Content-Range", contentRange, req); log.debug("Content Range: " + contentRange + " for " + file); log.debug("Content Length: " + size + " for " + file); @@ -163,6 +250,8 @@ public void doRangeRequest(File file, HttpServletRequest req, HttpServletRespons os.flush(); fis.close(); log.debug("Finished normal range stream for file " + file + "; processed " + counted + " bytes"); + } catch (IOException e) { + log.info("IOError ("+ file+ "): " + e.getMessage()); } catch (Throwable t) { log.warn("Range Stream Error for " + file, t); } finally { @@ -179,12 +268,12 @@ public long[] getRanges(String rangeHeader, long fileLength) { rangeHeader = rangeHeader.trim(); String bytes = rangeHeader.substring(rangeHeader.indexOf('=') + 1); if (bytes.length() == 0) { - return new long[] { 0, fileLength }; + return new long[] { 0, fileLength-1 }; } if (bytes.startsWith("-")) { return new long[] { 0, Long.valueOf(bytes.substring(1)) }; } else if (bytes.endsWith("-")) { - return new long[] { Long.valueOf(bytes.substring(0, bytes.length() - 1)), fileLength }; + return new long[] { Long.valueOf(bytes.substring(0, bytes.length() - 1)), fileLength-1 }; } else { int pos = bytes.indexOf("-"); return new long[] { Long.valueOf(bytes.substring(0, pos)), Long.valueOf(bytes.substring(pos + 1)) }; @@ -192,26 +281,6 @@ public long[] getRanges(String rangeHeader, long fileLength) { } private void doNormalRequest(File file, HttpServletRequest req, HttpServletResponse resp, Object sagefile) throws IOException { - resp.setHeader("Accept-Ranges", "bytes"); - resp.setHeader("Content-Length", String.valueOf(file.length())); - - log.debug("Setting Content-Length: " + file.length() + " for " + file); - - if (MediaFileAPI.IsMusicFile(sagefile)) { - resp.setContentType("audio/mpeg"); - } else if (MediaFileAPI.IsPictureFile(sagefile)) { - resp.setContentType("image/jpeg"); - } else { - resp.setContentType("video/mpeg"); - } - - String forceMime = req.getParameter("force-mime"); - if (forceMime != null && forceMime.length() > 0) { - resp.setContentType(forceMime); - } - - OutputStream os = resp.getOutputStream(); - MediaHandler.copyStream(new FileInputStream(file), os); - os.flush(); + doRangeRequestWithStatus(file, req, resp, sagefile, getRanges(req.getHeader("Range"), file.length())); } } diff --git a/src/main/java/sagex/remote/media/MediaHandler.java b/src/main/java/sagex/remote/media/MediaHandler.java index 173767b..5000e80 100644 --- a/src/main/java/sagex/remote/media/MediaHandler.java +++ b/src/main/java/sagex/remote/media/MediaHandler.java @@ -87,7 +87,7 @@ public void handleRequest(String args[], HttpServletRequest req, HttpServletResp if (args.length>4) { // we have a segment - req.setAttribute(MEDIA_SEGMENT_ATTRIBUTE, args[4]); + req.setAttribute(MEDIA_SEGMENT_ATTRIBUTE, parseSegment(args[4])); } if (req.getPathInfo().endsWith(".m3u") || req.getPathInfo().endsWith(".m3u8")) { @@ -108,7 +108,16 @@ public void handleRequest(String args[], HttpServletRequest req, HttpServletResp } } - private void help(HttpServletResponse resp, Throwable t) throws IOException { + private Object parseSegment(String seg) { + if (seg==null) return null; + int pos = seg.indexOf('.'); + if (pos!=-1) { + return seg.substring(0, pos); + } + return seg; + } + + private void help(HttpServletResponse resp, Throwable t) throws IOException { help(resp, t.getMessage(), t); } diff --git a/src/main/java/sagex/util/TypesUtil.java b/src/main/java/sagex/util/TypesUtil.java index e029abc..0b0f683 100644 --- a/src/main/java/sagex/util/TypesUtil.java +++ b/src/main/java/sagex/util/TypesUtil.java @@ -58,7 +58,7 @@ public static boolean toBoolean(String s, boolean defValue) { * @return */ public static Date toDate(String s) { - if (s==null) return null; + if (s==null||s.trim().length()==0) return null; try { long l = Long.parseLong(s); return new Date(l); @@ -70,7 +70,7 @@ public static Date toDate(String s) { // we'll log it later } } - log.warn("Failed to convert " + s + " to a valid date"); + log.warn("Failed to convert {" + s + "} to a valid date"); return null; }