public void send(Message message) { try { // force keyframe? ditch all the other messages on the queue // since this message will overwrite everything if (message.isBlockMessage() && ((BlockMessage) message).getForceKeyFrame()) { blockDataQ.clear(); notifyQueueListener(0); } // stick message on queue blockDataQ.put(message); // if the queue is backed up, purge it of duplicate blocks int qSizeInBlocks = getQueueSizeInBlocks(); if (qSizeInBlocks > ScreenShareInfo.MAX_QUEUE_SIZE_FOR_PAUSE) { if (ScreenShareInfo.getPurgeBackedUpQueue()) { purgeBlockDataQ(); } notifyQueueListener(qSizeInBlocks); } // if we're being slow, notify also if (ScreenShareInfo.adjustPauseFromQueueSize && perfStats.isSlow()) { notifyPerformanceListener(true); } } catch (InterruptedException e) { e.printStackTrace(); } }
@ThreadSafe public class NetworkStreamSender implements NextBlockRetriever, NetworkStreamListener { public static final String NAME = "NETWORKSTREAMSENDER: "; private ExecutorService executor; private LinkedBlockingQueue<Message> blockDataQ; private final int numThreads; private final String host; private final int port; private final String room; private final boolean httpTunnel; private NetworkSocketStreamSender[] socketSenders; private NetworkHttpStreamSender[] httpSenders; private boolean tunneling = false; private boolean stopped = true; private int numRunningThreads = 0; private Dimension screenDim; private Dimension blockDim; private BlockManager blockManager; private NetworkConnectionListener listener; private QueueListener queueListener; private PerformanceListener performanceListener; private final SequenceNumberGenerator seqNumGenerator = new SequenceNumberGenerator(); private PerformanceStats perfStats = PerformanceStats.getInstance(); public NetworkStreamSender( BlockManager blockManager, String host, int port, String room, Dimension screenDim, Dimension blockDim, boolean httpTunnel) { blockDataQ = new LinkedBlockingQueue<Message>(ScreenShareInfo.MAX_QUEUED_MESSAGES); this.blockManager = blockManager; this.host = host; this.port = port; this.room = room; this.screenDim = screenDim; this.blockDim = blockDim; this.httpTunnel = httpTunnel; numThreads = ScreenShareInfo.NETWORK_SENDER_COUNT; System.out.println(NAME + "Starting up " + numThreads + " sender threads."); executor = Executors.newFixedThreadPool(numThreads); } public void addNetworkConnectionListener(NetworkConnectionListener listener) { this.listener = listener; } public void addQueueListener(QueueListener queueListener) { this.queueListener = queueListener; } public void addPerformanceListener(PerformanceListener performanceListener) { this.performanceListener = performanceListener; } private void notifyNetworkConnectionListener(ExitCode reason) { if (listener != null) { listener.networkConnectionException(reason); } } private void notifyQueueListener(int queueSize) { if (queueListener != null) { if (queueSize == 0) { queueListener.onQueueCleared(); } else { queueListener.onQueueBackedup(queueSize); } } } private void notifyPerformanceListener(boolean slow) { if (performanceListener != null) { performanceListener.onSlowPerformance(slow); } } public boolean connect() { int failedAttempts = 0; socketSenders = new NetworkSocketStreamSender[numThreads]; try { NetworkSocket socket = new NetworkSocket(host, port); for (int i = 0; i < numThreads; i++) { try { createSender(i, socket); numRunningThreads++; } catch (ConnectionException e) { failedAttempts++; } } } catch (ConnectionException e) { e.printStackTrace(); failedAttempts = numThreads; } if ((failedAttempts == numThreads) && httpTunnel) { System.out.println(NAME + "Trying http tunneling"); failedAttempts = 0; numRunningThreads = 0; if (tryHttpTunneling()) { tunneling = true; System.out.println(NAME + "Will use http tunneling"); httpSenders = new NetworkHttpStreamSender[numThreads]; for (int i = 0; i < numThreads; i++) { try { createHttpSender(i); numRunningThreads++; } catch (ConnectionException e) { failedAttempts++; } } return failedAttempts != numThreads; } } else { if (numRunningThreads != numThreads) { try { stop(); } catch (ConnectionException e) { e.printStackTrace(); } return false; } else { return true; } } System.out.println(NAME + "Http tunneling failed."); return false; } private void createSender(int i, NetworkSocket socket) throws ConnectionException { socketSenders[i] = new NetworkSocketStreamSender(i, this, room, screenDim, blockDim, seqNumGenerator); socketSenders[i].addListener(this); socketSenders[i].connect(socket); } private void createHttpSender(int i) throws ConnectionException { httpSenders[i] = new NetworkHttpStreamSender(i, this, room, screenDim, blockDim, seqNumGenerator); httpSenders[i].addListener(this); httpSenders[i].connect(host); } /** * this examines the block data queue for messages that may include the same blocks and discards * any dupe blocks todo: refactor this nastiness */ private synchronized void purgeBlockDataQ() { Message message, nextMessage; BlockMessage blockMessage, nextBlockMessage; Integer[] blocks, nextBlocks; boolean skipMessage = false; int currentMessage = 0; LinkedBlockingQueue<Message> cloneQ = new LinkedBlockingQueue<Message>(ScreenShareInfo.MAX_QUEUED_MESSAGES); AtomicLong startTime = new AtomicLong(System.currentTimeMillis()); Iterator it = blockDataQ.iterator(); while (it.hasNext()) { message = (Message) it.next(); skipMessage = false; ++currentMessage; // we only care about block messages that are keyframes // and have another block message after them // to dedupe against if (!(message.isBlockMessage() && ((BlockMessage) message).getForceKeyFrame()) && it.hasNext()) { continue; } blockMessage = (BlockMessage) message; blocks = blockMessage.getBlocks(); // look at the next block message to see if it overlaps with this one nextMessage = null; while (it.hasNext()) { nextMessage = (Message) it.next(); if (!nextMessage.isBlockMessage()) { nextMessage = null; continue; } } // now compare the blocks between this msg and the next block message if (nextMessage != null) { nextBlockMessage = (BlockMessage) nextMessage; nextBlocks = nextBlockMessage.getBlocks(); // exactly the same? ditch the whole message if (nextBlockMessage.hasSameBlocksAs(blockMessage)) { skipMessage = true; } else { // not exactly the same? See if we can prune dupe blocks blockMessage.discardBlocksSharedWith(nextBlockMessage); skipMessage = (nextBlockMessage.isEmpty()); } } // if the current message must be retained, put it on the clone queue if (skipMessage) { continue; } try { cloneQ.put(message); } catch (InterruptedException e) { // System.out.println("Can't put message on clone queue"); } } // we've looked at the whole queue, so time to swap in clone for real queue try { blockDataQ.clear(); for (Message cloneMessage : cloneQ) { blockDataQ.put(cloneMessage); } } catch (InterruptedException e) { // System.out.println("Can't dump clone queue into queue"); } } public void send(Message message) { try { // force keyframe? ditch all the other messages on the queue // since this message will overwrite everything if (message.isBlockMessage() && ((BlockMessage) message).getForceKeyFrame()) { blockDataQ.clear(); notifyQueueListener(0); } // stick message on queue blockDataQ.put(message); // if the queue is backed up, purge it of duplicate blocks int qSizeInBlocks = getQueueSizeInBlocks(); if (qSizeInBlocks > ScreenShareInfo.MAX_QUEUE_SIZE_FOR_PAUSE) { if (ScreenShareInfo.getPurgeBackedUpQueue()) { purgeBlockDataQ(); } notifyQueueListener(qSizeInBlocks); } // if we're being slow, notify also if (ScreenShareInfo.adjustPauseFromQueueSize && perfStats.isSlow()) { notifyPerformanceListener(true); } } catch (InterruptedException e) { e.printStackTrace(); } } public void start() { System.out.println(NAME + "Starting network sender."); if (tunneling) { for (int i = 0; i < numRunningThreads; i++) { httpSenders[i].sendStartStreamMessage(); executor.execute(httpSenders[i]); } } else { for (int i = 0; i < numRunningThreads; i++) { try { socketSenders[i].sendStartStreamMessage(); executor.execute(socketSenders[i]); } catch (ConnectionException e) { e.printStackTrace(); } } } stopped = false; } public void stop() throws ConnectionException { stopped = true; System.out.println(NAME + "Stopping network sender"); for (int i = 0; i < numRunningThreads; i++) { if (tunneling) { httpSenders[i].disconnect(); } else { socketSenders[i].disconnect(); } } executor.shutdownNow(); httpSenders = null; socketSenders = null; } private boolean tryHttpTunneling() { NetworkHttpStreamSender httpSender = new NetworkHttpStreamSender(0, this, room, screenDim, blockDim, seqNumGenerator); try { httpSender.connect(host); return true; } catch (ConnectionException e) { System.out.println(NAME + "Problem connecting to " + host); } return false; } public void blockSent(int position) { blockManager.blockSent(position); } public EncodedBlockData getBlockToSend(int position) { return blockManager.getBlock(position).encode(); } public Message getNextMessageToSend() throws InterruptedException { try { return blockDataQ.take(); } catch (InterruptedException e) { if (!stopped) { e.printStackTrace(); } throw e; } } public void networkException(int id, ExitCode reason) { try { numRunningThreads--; if (tunneling) { // httpSenders[id].disconnect(); System.out.println(NAME + "Failed to use http tunneling. Stopping."); stop(); notifyNetworkConnectionListener(reason); } else { socketSenders[id].disconnect(); } if (numRunningThreads < 1) { System.out.println(NAME + "No more sender threads. Stopping."); stop(); notifyNetworkConnectionListener(reason); } else { System.out.println( NAME + "Sender thread stopped. " + numRunningThreads + " sender threads remaining."); } } catch (ConnectionException e) { // TODO Auto-generated catch block e.printStackTrace(); if (numRunningThreads < 1) { System.out.println(NAME + "No more sender threads. Stopping."); notifyNetworkConnectionListener(reason); } else { System.out.println( NAME + "Sender thread stopped. " + numRunningThreads + " sender threads remaining."); } } } // utility function, remove me private int getQueueSizeInBlocks() { int numBlocks = 0; for (Message message : blockDataQ) { if (message.isBlockMessage()) { numBlocks += ((BlockMessage) message).size(); } } return numBlocks; } }