/*- ******************************************************************************* * Copyright (c) 2016 Diamond Light Source Ltd. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Matthew Gerring - initial version 'AbstractStreamer' on which this class is based * Matthew Taylor - modified to be non-caching, misses sleeps to catch-up to source, and only processing the data when it's requested *******************************************************************************/ package org.eclipse.dawnsci.remotedataset.client.streamer; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import org.eclipse.dawnsci.remotedataset.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; abstract class AbstractNonCachingStreamer<T> implements IStreamer<T>, Runnable { protected static final Logger logger = LoggerFactory.getLogger(AbstractNonCachingStreamer.class); private BlockingQueue<byte[]> queue; private InputStream in; private long sleepTime; private long receivedImages = 0; private boolean isFinished; private String delimiter; /** * Initialises the connection * @param url * @param sleepTime * @return * @throws Exception */ protected URLConnection init(URL url, long sleepTime) throws Exception { URLConnection conn = url.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); conn.setUseCaches(false); String contentType = conn.getContentType(); if (!contentType.startsWith(Constants.MCONTENT_TYPE)) throw new Exception("getImages() may only be used with "+Constants.MCONTENT_TYPE+", not "+contentType); this.delimiter = contentType.split("boundary=")[1]; this.queue = new LinkedBlockingQueue<byte[]>(1); this.in = new BufferedInputStream(conn.getInputStream()); this.sleepTime = sleepTime; return conn; } /** * Runs until finished or the stream is closed, continuously parsing the data in the stream to form an image */ public void run() { isFinished = false; try { final StringBuilder buf = new StringBuilder(); int c = -1; boolean foundImage = false; int bytesAvailableAfterLastSleep = in.available(); int bytesReadSinceLastSleep = 0; while(!isFinished && (c=in.read())> -1 ) { bytesReadSinceLastSleep++; buf.append((char)c); if (buf.length()>0 && buf.charAt(buf.length()-1) == '\n') { // Line found final String line = buf.toString().trim(); if (line.equals("--"+delimiter)) { // We found a new image foundImage = true; } if (foundImage && line.startsWith("Content-Length: ")) { int clength = Integer.parseInt(line.split("\\:")[1].trim()); readImage(in, clength); if (isFinished) return; foundImage = false; bytesReadSinceLastSleep+=clength; // We don't want to use all the CPU so sleep unless there's a lot more data in the buffer we haven't read since the last sleep. // We don't want the buffer to build up too far ahead, so we don't sleep in order to catch up if (bytesReadSinceLastSleep >= bytesAvailableAfterLastSleep) { Thread.sleep(sleepTime); bytesAvailableAfterLastSleep = in.available(); bytesReadSinceLastSleep = 0; } } buf.delete(0, buf.length()); continue; } } } catch (Exception ne) { setFinished(true); logger.error("Cannot read input stream in "+getClass().getSimpleName(), ne); } finally { setFinished(true); try { in.close(); } catch (Exception ne) { logger.error("Cannot close connection!", ne); } // Cannot have null, instead add tiny empty image queue.clear(); queue.offer(new byte[]{}); } } /** * Reads an image from the stream, populating the array with the latest image bytes * @param in * @param clength * @throws Exception */ private void readImage(InputStream in, int clength) throws Exception { int c= -1; // Scoot down until no more new lines (this loses first character of JPG) while((c=in.read())> -1) { if (c=='\r') continue; if (c=='\n') continue; break; } byte[] imageBytes = new byte[clength + 1]; imageBytes[0] = (byte)c; // We took one int offset = 1; int numRead = 0; while (!isFinished && offset < imageBytes.length && (numRead=in.read(imageBytes, offset, imageBytes.length-offset)) >= 0) { offset += numRead; } if (isFinished) return; queue.clear(); queue.offer(imageBytes); } /** * Implement to turn the raw stream bytes into type T * @param bais * @return * @throws Exception */ protected abstract T getFromStream(ByteArrayInputStream bais) throws Exception; /** * Blocks until image added, after that will take the latest data. Once null is added, we are done. * @return Image or null when finished. * * @throws InterruptedException */ public T take() throws InterruptedException { byte[] latestBytes = queue.take(); // Might get interrupted ByteArrayInputStream bais = new ByteArrayInputStream(latestBytes); T bi = null; try { bi = getFromStream(bais); if (isFinished || bi == getQueueEndObject()) { setFinished(true); return null; } } catch (Exception e) { e.printStackTrace(); } receivedImages++; return bi; } /** * Implement to designate an object of tpye T as the end of queue object * @return */ protected abstract T getQueueEndObject(); /** * Gets the dropped image count. Note, this does not apply to this streamer, so will return 0 */ public long getDroppedImageCount() { return 0; } /** * Gets the received image count */ public long getReceivedImageCount() { return receivedImages; } /** * Starts the thread running */ public void start() { Thread thread = new Thread(this); thread.setPriority(Thread.MIN_PRIORITY); thread.setDaemon(true); thread.setName("MJPG Streamer"); thread.start(); } /** * Call to tell the streamer to stop adding images to its queue. * @param b */ public void setFinished(boolean b) { this.isFinished = b; } }