diff --git a/server/src/main/java/ctbrec/recorder/server/HlsServlet.java b/server/src/main/java/ctbrec/recorder/server/HlsServlet.java index 51f89745..eeab51f8 100644 --- a/server/src/main/java/ctbrec/recorder/server/HlsServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/HlsServlet.java @@ -2,11 +2,13 @@ package ctbrec.recorder.server; import static javax.servlet.http.HttpServletResponse.*; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; +import java.io.RandomAccessFile; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Enumeration; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -15,16 +17,13 @@ import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.iheartradio.m3u8.ParseException; -import com.iheartradio.m3u8.PlaylistException; - import ctbrec.Config; public class HlsServlet extends AbstractCtbrecServlet { private static final transient Logger LOG = LoggerFactory.getLogger(HlsServlet.class); - private Config config; + private final Config config; public HlsServlet(Config config) { this.config = config; @@ -42,34 +41,38 @@ public class HlsServlet extends AbstractCtbrecServlet { try { boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI()); if (!isRequestAuthenticated) { - resp.setStatus(SC_UNAUTHORIZED); - String response = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}"; - resp.getWriter().write(response); + writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}"); return; } } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - resp.setStatus(SC_UNAUTHORIZED); - String response = "{\"status\": \"error\", \"msg\": \"Authentication failed\"}"; - resp.getWriter().write(response); + writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"Authentication failed\"}"); return; } - try { - servePlaylist(req, resp, requestedFile); - } catch (ParseException | PlaylistException e) { - LOG.error("Error while generating playlist file", e); - throw new IOException("Couldn't generate playlist file " + requestedFile, e); - } + servePlaylist(req, resp, requestedFile); } else { if (requestedFile.exists()) { + Enumeration headerNames = req.getHeaderNames(); + while(headerNames.hasMoreElements()) { + String header = headerNames.nextElement(); + LOG.info("{}: {}", header, req.getHeader(header)); + } serveSegment(req, resp, requestedFile); } else { error404(req, resp); } } } else { - resp.setStatus(HttpServletResponse.SC_FORBIDDEN); - resp.getWriter().println("Stop it!"); + writeResponse(resp, SC_FORBIDDEN, "Stop it!"); + } + } + + private void writeResponse(HttpServletResponse resp, int code, String body) { + try { + resp.setStatus(code); + resp.getWriter().write(body); + } catch (IOException e) { + LOG.error("Couldn't write HTTP response", e); } } @@ -77,23 +80,32 @@ public class HlsServlet extends AbstractCtbrecServlet { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); } - private void serveSegment(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws FileNotFoundException, IOException { - serveFile(resp, requestedFile, "application/octet-stream"); + private void serveSegment(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException { + String mimetype = requestedFile.getName().endsWith(".mp4") ? "video/mp4" : "application/octet-stream"; + serveFile(req, resp, requestedFile, mimetype); } - private void servePlaylist(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws FileNotFoundException, IOException, ParseException, PlaylistException { - serveFile(resp, requestedFile, "application/x-mpegURL"); + private void servePlaylist(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException { + serveFile(req, resp, requestedFile, "application/x-mpegURL"); } - private void serveFile(HttpServletResponse resp, File file, String contentType) throws FileNotFoundException, IOException { - LOG.trace("Serving segment {}", file.getAbsolutePath()); - resp.setStatus(200); - resp.setContentLength((int) file.length()); + private void serveFile(HttpServletRequest req, HttpServletResponse resp, File file, String contentType) throws IOException { + ByteRange range = getByteRange(req, file); + LOG.info("Serving segment {} range: {}-{}", file.getAbsolutePath(), range.from, range.to); + resp.setStatus(range.set ? SC_PARTIAL_CONTENT : SC_OK); resp.setContentType(contentType); - try(FileInputStream fin = new FileInputStream(file)) { + try (RandomAccessFile fin = new RandomAccessFile(file, "r")) { + fin.seek(range.from); byte[] buffer = new byte[1024 * 100]; + long bytesLeft = range.to - range.from; + resp.setContentLengthLong(bytesLeft); + if (range.set) { + resp.setHeader("Content-Range", "bytes " + range.from + '-' + range.to + '/' + file.length()); + } + int bytesToRead = (int) Math.min(bytesLeft, buffer.length); int length = -1; - while( (length = fin.read(buffer)) >= 0) { + while ((length = fin.read(buffer, 0, bytesToRead)) >= 0) { + bytesLeft -= length; resp.getOutputStream().write(buffer, 0, length); } } @@ -103,4 +115,28 @@ public class HlsServlet extends AbstractCtbrecServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } + + private ByteRange getByteRange(HttpServletRequest req, File file) { + ByteRange range = new ByteRange(); + range.to = file.length(); + String rangeHeader = req.getHeader("Range"); + if (rangeHeader != null) { + range.set = true; + Matcher m = Pattern.compile("bytes=(\\d+)-(\\d+)*").matcher(rangeHeader); + if (m.find()) { + range.from = Long.parseLong(m.group(1)); + String to = m.group(2); + if (to != null && !to.trim().isEmpty()) { + range.to = Long.parseLong(to); + } + } + } + return range; + } + + class ByteRange { + boolean set = false; + long from = 0; + long to = Long.MAX_VALUE; + } }