package com.github.kmkt.util.mjpeg; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * MJPEG over HTTP servlet * * License : MIT License */ public class MjpegServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger logger = LoggerFactory.getLogger(MjpegServlet.class); public static long StatisticsDispleyPeriod = 60*1000; // private static final byte[] CRLF = new byte[]{0x0d, 0x0a}; private static final String CONTENT_TYPE = "multipart/x-mixed-replace"; private Set<ClientChannel> clientConnectionSet = new CopyOnWriteArraySet<ClientChannel>(); class ClientChannel { final BlockingQueue<byte[]> frameServer = new SynchronousQueue<byte[]>(); // statistics long channelOpenedAt = 0; long lastShownStatistics = 0; final AtomicLong recvedFrames = new AtomicLong(); final AtomicLong sentFrames = new AtomicLong(); final AtomicLong dropFrames = new AtomicLong(); final AtomicLong recvedBytes = new AtomicLong(); final AtomicLong sentBytes = new AtomicLong(); } /** * JPEG フレームデータを供給する。 * * <pre> * MJPEG とする JPEG フレームデータを与える。 * 与えられたフレームはGET接続時に MJPEG over HTTP でクライアントに送られる。 * クライアント接続時には pourFrame でフレームデータが与えられ次第、クライアントにその * フレームが送信される。 * クライアントから接続されていない場合、与えられたフレームデータは破棄される。 * </pre> * * @param frame JPEG フレームデータ */ public void pourFrame(byte[] frame) { for (ClientChannel client : clientConnectionSet) { client.recvedFrames.incrementAndGet(); client.recvedBytes.addAndGet(frame.length); if (!client.frameServer.offer(frame)) { client.dropFrames.incrementAndGet(); } } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { logger.debug("doGet"); String remote = req.getRemoteAddr() + ":" + req.getRemotePort(); ClientChannel client = new ClientChannel(); client.channelOpenedAt = System.currentTimeMillis(); client.lastShownStatistics = client.channelOpenedAt; try { clientConnectionSet.add(client); logger.debug("queueSet size : {}", clientConnectionSet.size()); logger.info("Accept HTTP connection from {}", remote); String delemeter_str = Long.toHexString(System.currentTimeMillis()); byte[] delimiter = ("--"+delemeter_str).getBytes(); byte[] content_type = "Content-Type: image/jpeg".getBytes(); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentType(CONTENT_TYPE+";boundary=" + delemeter_str); resp.setHeader("Connection", "Close"); BlockingQueue<byte[]> frameServer = client.frameServer; OutputStream out = new BufferedOutputStream(resp.getOutputStream()); try { frameServer.clear(); int i=-1; while (true) { byte[] frame = frameServer.poll(10, TimeUnit.SECONDS); if (frame == null) continue; byte[] content_length = ("Content-Length: " + frame.length).getBytes(); i++; logger.trace("Send frame {}", i); out.write(delimiter); out.write(CRLF); out.write(content_type); out.write(CRLF); out.write(content_length); out.write(CRLF); out.write(CRLF); out.write(frame); out.write(CRLF); out.flush(); client.sentFrames.incrementAndGet(); client.sentBytes.addAndGet(frame.length); if (StatisticsDispleyPeriod < System.currentTimeMillis() - client.lastShownStatistics) { client.lastShownStatistics = System.currentTimeMillis(); logger.debug("Statistics of {} [Frames Recv: {}, Send: {}, Drop: {}, Size Recv: {}, Send: {}]", remote, client.recvedFrames.get(), client.sentFrames.get(), client.dropFrames.get(), client.recvedBytes.get(), client.sentBytes.get()); } } } catch (IOException e) { // connection closed logger.info("Close HTTP connection from {}", remote); } catch (InterruptedException e) { logger.info(e.getMessage(), e); } } finally { clientConnectionSet.remove(client); logger.debug("queueSet size : {}", clientConnectionSet.size()); } } }