001 package com.github.sarxos.webcam; 002 003 import java.awt.image.BufferedImage; 004 import java.io.BufferedOutputStream; 005 import java.io.BufferedReader; 006 import java.io.ByteArrayOutputStream; 007 import java.io.IOException; 008 import java.io.InputStreamReader; 009 import java.net.ServerSocket; 010 import java.net.Socket; 011 import java.net.SocketException; 012 import java.util.concurrent.Executor; 013 import java.util.concurrent.Executors; 014 import java.util.concurrent.ThreadFactory; 015 import java.util.concurrent.atomic.AtomicBoolean; 016 017 import javax.imageio.ImageIO; 018 019 import org.slf4j.Logger; 020 import org.slf4j.LoggerFactory; 021 022 023 public class WebcamStreamer implements ThreadFactory, WebcamListener { 024 025 private static final Logger LOG = LoggerFactory.getLogger(WebcamStreamer.class); 026 027 private static final String BOUNDARY = "mjpegframe"; 028 029 private class Acceptor implements Runnable { 030 031 @Override 032 public void run() { 033 try { 034 ServerSocket server = new ServerSocket(port); 035 while (true) { 036 executor.execute(new Connection(server.accept())); 037 } 038 } catch (Exception e) { 039 LOG.error("Cannot accept socket connection", e); 040 } 041 } 042 } 043 044 private class Reader implements Runnable { 045 046 @Override 047 public void run() { 048 while (webcam.isOpen()) { 049 050 image = webcam.getImage(); 051 052 if (image != null) { 053 synchronized (lock) { 054 lock.notifyAll(); 055 } 056 } 057 058 try { 059 Thread.sleep(getDelay()); 060 } catch (InterruptedException e) { 061 LOG.error("Nasty interrupted exception", e); 062 } 063 } 064 } 065 } 066 067 private class Connection implements Runnable { 068 069 private static final String CRLF = "\r\n"; 070 071 private Socket socket = null; 072 073 public Connection(Socket socket) { 074 this.socket = socket; 075 } 076 077 @Override 078 public void run() { 079 080 BufferedReader br = null; 081 BufferedOutputStream bos = null; 082 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 083 084 try { 085 086 br = new BufferedReader(new InputStreamReader(socket.getInputStream())); 087 bos = new BufferedOutputStream(socket.getOutputStream()); 088 089 try { 090 091 while (webcam.isOpen()) { 092 093 if (socket.isInputShutdown()) { 094 break; 095 } 096 if (socket.isClosed()) { 097 break; 098 } 099 100 String line = br.readLine(); 101 if (line == null || line.isEmpty()) { 102 bos.write(("--" + BOUNDARY + "--" + CRLF).getBytes()); 103 LOG.info("Breaking"); 104 break; 105 } 106 107 if (line.startsWith("GET")) { 108 109 socket.setSoTimeout(0); 110 socket.setKeepAlive(true); 111 112 getImage(); 113 114 StringBuilder sb = new StringBuilder(); 115 sb.append("HTTP/1.0 200 OK").append(CRLF); 116 sb.append("Connection: keep-alive").append(CRLF); 117 sb.append("Cache-Control: no-cache").append(CRLF); 118 sb.append("Cache-Control: private").append(CRLF); 119 sb.append("Pragma: no-cache").append(CRLF); 120 sb.append("Content-type: multipart/x-mixed-replace; boundary=--").append(BOUNDARY).append(CRLF); 121 sb.append(CRLF); 122 123 bos.write(sb.toString().getBytes()); 124 125 do { 126 127 if (socket.isInputShutdown()) { 128 break; 129 } 130 if (socket.isClosed()) { 131 break; 132 } 133 134 baos.reset(); 135 136 ImageIO.write(getImage(), "JPG", baos); 137 138 sb.delete(0, sb.length()); 139 sb.append("--").append(BOUNDARY).append(CRLF); 140 sb.append("Content-type: image/jpeg").append(CRLF); 141 sb.append("Content-Length: ").append(baos.size()).append(CRLF); 142 sb.append(CRLF); 143 144 bos.write(sb.toString().getBytes()); 145 bos.write(baos.toByteArray()); 146 bos.write(CRLF.getBytes()); 147 148 try { 149 bos.flush(); 150 } catch (SocketException e) { 151 if (LOG.isTraceEnabled()) { 152 LOG.error("Socket exception", e); 153 } 154 } 155 156 Thread.sleep(getDelay()); 157 158 } while (webcam.isOpen()); 159 } 160 } 161 } catch (Exception e) { 162 163 String message = e.getMessage(); 164 if (message != null && message.startsWith("Software caused connection abort")) { 165 LOG.info("User closed stream"); 166 return; 167 } 168 169 LOG.error("Error", e); 170 171 bos.write("HTTP/1.0 501 Internal Server Error\r\n\r\n\r\n".getBytes()); 172 } 173 174 } catch (IOException e) { 175 LOG.error("I/O exception", e); 176 } finally { 177 if (br != null) { 178 try { 179 br.close(); 180 } catch (IOException e) { 181 LOG.error("Cannot close buffered reader", e); 182 } 183 } 184 if (bos != null) { 185 try { 186 bos.close(); 187 } catch (IOException e) { 188 LOG.error("Cannot close data output stream", e); 189 } 190 } 191 } 192 } 193 } 194 195 private Webcam webcam = null; 196 private double fps = 0; 197 private BufferedImage image = null; 198 private Object lock = new Object(); 199 private int number = 0; 200 private int port = 0; 201 private Executor executor = Executors.newCachedThreadPool(this); 202 private AtomicBoolean open = new AtomicBoolean(false); 203 private AtomicBoolean initialized = new AtomicBoolean(false); 204 205 public WebcamStreamer(int port, Webcam webcam, double fps, boolean start) { 206 207 if (webcam == null) { 208 throw new IllegalArgumentException("Webcam for streaming cannot be null"); 209 } 210 211 this.port = port; 212 this.webcam = webcam; 213 this.fps = fps; 214 215 if (start) { 216 start(); 217 } 218 } 219 220 private long getDelay() { 221 return (long) (1000 / fps); 222 } 223 224 private BufferedImage getImage() { 225 if (image == null) { 226 synchronized (lock) { 227 try { 228 lock.wait(); 229 } catch (InterruptedException e) { 230 LOG.error("Nasty interrupted exception", e); 231 } 232 } 233 } 234 return image; 235 } 236 237 public void start() { 238 if (open.compareAndSet(false, true)) { 239 webcam.addWebcamListener(this); 240 webcam.open(); 241 } 242 } 243 244 @Override 245 public Thread newThread(Runnable r) { 246 Thread thread = new Thread(r, String.format("streamer-thread-%s", number++)); 247 thread.setDaemon(true); 248 return thread; 249 } 250 251 @Override 252 public void webcamOpen(WebcamEvent we) { 253 if (initialized.compareAndSet(false, true)) { 254 executor.execute(new Acceptor()); 255 executor.execute(new Reader()); 256 } 257 } 258 259 @Override 260 public void webcamClosed(WebcamEvent we) { 261 // TODO: shutdown executor? 262 } 263 264 @Override 265 public void webcamDisposed(WebcamEvent we) { 266 } 267 268 @Override 269 public void webcamImageObtained(WebcamEvent we) { 270 } 271 272 public static void main(String[] args) throws InterruptedException { 273 new WebcamStreamer(8081, Webcam.getDefault(), 0.5, true); 274 do { 275 Thread.sleep(1000); 276 } while (true); 277 } 278 }