/* * PS3 Media Server, for streaming any medias to your PS3. * Copyright (C) 2008 A.Brochard * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.io; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; import java.util.Timer; import java.util.TimerTask; import net.pms.PMS; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Circular memory buffer that can be used as {@link java.io.OutputStream OutputStream} * and provides methods that can read data from the memory buffer using an * {@link java.io.InputStream InputStream}. The name of this class is a bit * misleading, as there is typically no file involved in the process at all. * Instead, the buffer is typically used to hold data piped by a transcoding * process in one thread until a request for data comes in from another thread. * * @see ProcessWrapperImpl * @see net.pms.network.Request Request * @see net.pms.network.RequestV2 RequestV2 */ public class BufferedOutputFileImpl extends OutputStream implements BufferedOutputFile { private static final Logger LOGGER = LoggerFactory.getLogger(BufferedOutputFileImpl.class); private PmsConfiguration configuration; /** * Initial size for the buffer in bytes. * The current value is 50MB. */ private static final int INITIAL_BUFFER_SIZE = 52428800; /** * Amount of extra bytes to increase the initial buffer with when memory * allocation fails. */ private static final int MARGIN_LARGE = 20000000; private static final int MARGIN_MEDIUM = 2000000; private static final int MARGIN_SMALL = 600000; private static final int CHECK_INTERVAL = 500; private static final int CHECK_END_OF_PROCESS = 2500; // must be superior to CHECK_INTERVAL private int minMemorySize; private int maxMemorySize; private int bufferOverflowWarning; private boolean eof; private long writeCount; private byte buffer[]; private boolean forcefirst; private ArrayList<WaitBufferedInputStream> inputStreams; private ProcessWrapper attachedThread; private int secondread_minsize; private Timer timer; private boolean hidebuffer; private boolean cleanup; private boolean shiftScr; private FileOutputStream debugOutput = null; private boolean buffered = false; private NumberFormat formatter = NumberFormat.getInstance(Locale.US); private double timeseek; private double timeend; private long packetpos = 0; private final RendererConfiguration renderer; /** * Try to increase the size of a memory buffer, while retaining its * contents. The provided new size is considered to be a request, it is * scaled down when an OutOfMemory error occurs. There is no guarantee * about the exact length of the returned byte array, only that it is * greater than or equal to the original buffer size. When null is * passed as an argument, a fresh buffer will be allocated. Copying one * byte array to another is a costly operation, both in memory usage and * performance. It is best to avoid using this method. * * @param buffer The byte array to resize, null is allowed. * @param newSize The requested final size. Should be greater than the * original size or the original buffer will be returned. * @return The resized byte array. */ private byte[] growBuffer(byte[] buffer, int newSize) { byte[] copy; if (buffer == null) { // Temporary empty array to avoid null tests in the code below buffer = new byte[0]; } if (newSize <= buffer.length) { // Cannot shrink the original return buffer; } try { // Try to allocate the requested new size copy = new byte[newSize]; } catch (OutOfMemoryError e) { if (buffer.length == 0) { LOGGER.trace("Cannot initialize buffer to " + formatter.format(newSize) + " bytes."); } else { LOGGER.debug("Cannot grow buffer size from " + formatter.format(buffer.length) + " bytes to " + formatter.format(newSize) + " bytes."); LOGGER.debug("Error given: " + e); } // Could not allocate the requested new size, use 30% of free memory instead. // Rationale behind using 30%: multiple threads are running at the same time, // we do not want one thread's memory usage to suffocate the others. // Using maxMemory() to ignore the initial Java heap space size that freeMemory() // takes into account. // See http://javarevisited.blogspot.com/2011/05/java-heap-space-memory-size-jvm.html long realisticSize = Runtime.getRuntime().maxMemory() * 3 / 10; if (realisticSize < buffer.length) { // A copy would be smaller in size, shrinking instead of growing the buffer. // Better to return the original and retain its size. return buffer; } else { try { // Try to allocate the realistic alternative size copy = new byte[(int) realisticSize]; } catch (OutOfMemoryError e2) { LOGGER.debug("Cannot grow buffer size from " + formatter.format(buffer.length) + " bytes to " + formatter.format(realisticSize) + " bytes either."); LOGGER.trace("freeMemory: " + formatter.format(Runtime.getRuntime().freeMemory())); LOGGER.trace("totalMemory: " + formatter.format(Runtime.getRuntime().totalMemory())); LOGGER.trace("maxMemory: " + formatter.format(Runtime.getRuntime().maxMemory())); LOGGER.debug("Error given: " + e2); // Cannot allocate memory, no other option than to return the original. return buffer; } } } if (buffer.length == 0) { LOGGER.trace("Successfully initialized buffer to " + formatter.format(copy.length) + " bytes."); } else { try { System.arraycopy(buffer, 0, copy, 0, buffer.length); LOGGER.trace("Successfully grown buffer from " + formatter.format(buffer.length) + " bytes to " + formatter.format(copy.length) + " bytes."); } catch (NullPointerException npe) { LOGGER.trace("Cannot grow buffer size, error copying buffer contents."); } } return copy; } /** * Constructor to create a memory buffer based on settings that are * passed on. Will also start up a timer task to display buffer size and * usage in the PMS main screen. * * @param params {@link OutputParams} object that contains preferences * for the buffers dimensions and behavior. */ public BufferedOutputFileImpl(OutputParams params) { // Use device-specific pms conf configuration = PMS.getConfiguration(params); this.renderer = params.mediaRenderer; this.forcefirst = (configuration.getTrancodeBlocksMultipleConnections() && configuration.getTrancodeKeepFirstConnections()); this.minMemorySize = (int) (1048576 * params.minBufferSize); this.maxMemorySize = (int) (1048576 * params.maxBufferSize); // FIXME: Better to relate margin directly to maxMemorySize instead of using arbitrary fixed values int margin = MARGIN_LARGE; // Issue 220: extends to 20Mb : readCount is wrongly set cause of the ps3's // 2nd request with a range like 44-xxx, causing the end of buffer margin to be first sent if (this.maxMemorySize < margin) {// for thumbnails / small buffer usage margin = MARGIN_MEDIUM; // margin must be superior to the buffer size of OutputBufferConsumer or direct buffer size from WindowsNamedPipe class if (this.maxMemorySize < margin) { margin = MARGIN_SMALL; } } this.bufferOverflowWarning = this.maxMemorySize - margin; this.secondread_minsize = params.secondread_minsize; this.timeseek = params.timeseek; this.timeend = params.timeend; this.shiftScr = params.shift_scr; this.hidebuffer = params.hidebuffer; this.cleanup = params.cleanup; if (maxMemorySize > INITIAL_BUFFER_SIZE) { // Try to limit memory usage a bit. // Start with a modest allocation initially, grow to max when needed later. buffer = growBuffer(null, INITIAL_BUFFER_SIZE); } else { buffer = growBuffer(null, maxMemorySize); } if (buffer.length == 0) { // Cannot transcode without a buffer LOGGER.info("FATAL ERROR: OutOfMemory / dumping stats"); LOGGER.trace("freeMemory: " + Runtime.getRuntime().freeMemory()); LOGGER.trace("totalMemory: " + Runtime.getRuntime().totalMemory()); LOGGER.trace("maxMemory: " + Runtime.getRuntime().maxMemory()); System.exit(1); } inputStreams = new ArrayList<>(); } @Override public void close() throws IOException { LOGGER.trace("EOF"); eof = true; if (cleanup) { detachInputStream(); } } @Override public WaitBufferedInputStream getCurrentInputStream() { WaitBufferedInputStream wai = null; if (inputStreams.size() > 0) { try { wai = forcefirst ? inputStreams.get(0) : inputStreams.get(inputStreams.size() - 1); } catch (IndexOutOfBoundsException e) { // this should never happen unless there's a concurrency issue, // so log it if it does LOGGER.error("Unexpected input stream removal", e); } } return wai; } @Override public InputStream getInputStream(long newReadPosition) { if (attachedThread != null) { attachedThread.setReadyToStop(false); } WaitBufferedInputStream atominputStream; if (!configuration.getTrancodeBlocksMultipleConnections() || getCurrentInputStream() == null) { atominputStream = new WaitBufferedInputStream(this); inputStreams.add(atominputStream); } else { if (configuration.getTrancodeKeepFirstConnections()) { LOGGER.debug("BufferedOutputFile is already attached to an InputStream: " + getCurrentInputStream()); } else { // Ditlew - fixes the above (the above iterator breaks on items getting close, cause they will remove them self from the arraylist) while (inputStreams.size() > 0) { try { inputStreams.get(0).close(); } catch (IOException e) { LOGGER.error("Error: ", e); } } inputStreams.clear(); atominputStream = new WaitBufferedInputStream(this); inputStreams.add(atominputStream); LOGGER.debug("Reassign inputstream: " + getCurrentInputStream()); } return null; } if (newReadPosition > 0) { LOGGER.debug("Setting InputStream new position to: " + formatter.format(newReadPosition)); atominputStream.setReadCount(newReadPosition); } return atominputStream; } @Override public long getWriteCount() { return writeCount; } @Override public void write(byte b[], int off, int len) throws IOException { if (debugOutput != null) { debugOutput.write(b, off, len); debugOutput.flush(); } WaitBufferedInputStream input = getCurrentInputStream(); //LOGGER.trace("write(" + b.length + ", " + off + ", " + len + "), writeCount = " + writeCount + ", readCount = " + (input != null ? input.getReadCount() : "null")); while ((input != null && (writeCount - input.getReadCount() > bufferOverflowWarning)) || (input == null && writeCount > bufferOverflowWarning)) { try { Thread.sleep(CHECK_INTERVAL); } catch (InterruptedException e) { } input = getCurrentInputStream(); } if (buffer != null) { int mb = (int) (writeCount % maxMemorySize); if (mb >= buffer.length - (len - off)) { if (buffer.length == INITIAL_BUFFER_SIZE) { // Initial buffer size was not big enough, try to increase it buffer = growBuffer(buffer, maxMemorySize); } // FIXME: This smells like 2x System.arraycopy()! int s = (len - off); for (int i = 0; i < s; i++) { buffer[modulo(mb + i, buffer.length)] = b[off + i]; } } else { System.arraycopy(b, off, buffer, mb, (len - off)); if ((len - off) > 0) { buffered = true; } } // Ditlew - WDTV Live if (timeseek > 0 && writeCount > 10) { for (int i = 0; i < len; i++) { if (buffer != null && shiftScr) { shiftSCRByTimeSeek(mb + i, (int) timeseek); // Ditlew - update any SCR headers } //shiftGOPByTimeSeek(mb+i, (int)timeseek); // Ditlew - update any GOP headers - Not needed for WDTV Live } } writeCount += len - off; if (timeseek > 0 && timeend == 0) { int packetLength = 6; // minimum to get packet size while (packetpos + packetLength < writeCount && buffer != null) { int packetposMB = (int) (packetpos % maxMemorySize); int streamPos = 0; if (buffer[modulo(packetposMB, buffer.length)] == 71) {// TS packetLength = 188; streamPos = 4; // adaptation field if ((buffer[modulo(packetposMB + 3, buffer.length)] & 0x20) == 0x20) { streamPos += 1 + ((buffer[modulo(packetposMB + 4, buffer.length)] + 256) % 256); } if (streamPos == 188) { streamPos = -1; } } else if (buffer[modulo(packetposMB + 3, buffer.length)] == -70) { // BA packetLength = 14; streamPos = -1; } else { packetLength = 6 + (((buffer[modulo(packetposMB + 4, buffer.length)] + 256) % 256)) * 256 + ((buffer[modulo(packetposMB + 5, buffer.length)] + 256) % 256); } if (streamPos != -1) { mb = packetposMB + streamPos + 18; if (!shiftVideo(mb, true)) { mb -= 5; shiftAudio(mb, true); } } packetpos += packetLength; } } } } /** * Determine a modulo value that is guaranteed to be zero or positive, * as opposed to the standard Java % operator which can return a * negative value. * * @param number Number to divide * @param divisor Number that is used to divide * @return The rest value of the division. */ private int modulo(int number, int divisor) { if (number >= 0) { return number % divisor; } return ((number % divisor) + divisor) % divisor; } @Override public void write(int b) throws IOException { boolean bb = b % 100000 == 0; WaitBufferedInputStream input = getCurrentInputStream(); while (bb && ((input != null && (writeCount - input.getReadCount() > bufferOverflowWarning)) || (input == null && writeCount == bufferOverflowWarning))) { try { Thread.sleep(CHECK_INTERVAL); //LOGGER.trace("BufferedOutputFile Full"); } catch (InterruptedException e) { } input = getCurrentInputStream(); } int mb = (int) (writeCount++ % maxMemorySize); if (buffer != null) { buffer[mb] = (byte) b; buffered = true; if (writeCount == INITIAL_BUFFER_SIZE) { buffer = growBuffer(buffer, maxMemorySize); } if (timeseek > 0 && writeCount > 19) { shiftByTimeSeek(mb, mb <= 20); } // Ditlew - WDTV Live - update any SCR headers if (timeseek > 0 && writeCount > 10) { shiftSCRByTimeSeek(mb, (int) timeseek); } } } // Ditlew - Modify SCR private void shiftSCRByTimeSeek(int buffer_index, int offset_sec) { int m9 = modulo(buffer_index - 9, buffer.length); int m8 = modulo(buffer_index - 8, buffer.length); int m7 = modulo(buffer_index - 7, buffer.length); int m6 = modulo(buffer_index - 6, buffer.length); int m5 = modulo(buffer_index - 5, buffer.length); int m4 = modulo(buffer_index - 4, buffer.length); int m3 = modulo(buffer_index - 3, buffer.length); int m2 = modulo(buffer_index - 2, buffer.length); int m1 = modulo(buffer_index - 1, buffer.length); int m0 = modulo(buffer_index, buffer.length); // SCR if (buffer[m9] == 0 && buffer[m8] == 0 && buffer[m7] == 1 && buffer[m6] == -70 && // 0xBA - Java/PMS wants -70 // control bits !((buffer[m5] & 128) == 128) && ((buffer[m5] & 64) == 64) && ((buffer[m5] & 4) == 4) && ((buffer[m3] & 4) == 4) && ((buffer[m1] & 4) == 4) && ((buffer[m0] & 1) == 1)) { long scr_32_30 = ((buffer[m5] & 56) >> 3); long scr_29_15 = ((buffer[m5] & 3) << 13) + (buffer[m4] << 5) + ((buffer[m3] & 248) >> 3); long scr_14_00 = ((buffer[m3] & 3) << 13) + (buffer[m2] << 5) + ((buffer[m1] & 248) >> 3); long scr = (scr_32_30 << 30) + (scr_29_15 << 15) + scr_14_00; long scr_new = scr + (90000L * offset_sec); long scr_32_30_new = (scr_new & 7516192768L) >> 30; // 111000000000000000000000000000000 long scr_29_15_new = (scr_new & 1073709056L) >> 15; // 000111111111111111000000000000000 long scr_14_00_new = (scr_new & 32767L); // 000000000000000000111111111111111 // scr_32_30_new buffer[m5] = (byte) ((buffer[m5] & 199) + ((scr_32_30_new << 3) & 56)); // 11000111 // scr_29_15_new buffer[m5] = (byte) ((buffer[m5] & 252) + ((scr_29_15_new >> 13) & 3)); // 00000011 buffer[m4] = (byte) (scr_29_15_new >> 5); // 11111111 buffer[m3] = (byte) ((buffer[m3] & 7) + ((scr_29_15_new << 3) & 248)); // 11111000 // scr_14_00_new buffer[m3] = (byte) ((buffer[m3] & 252) + ((scr_14_00_new >> 13) & 3)); // 00000011 buffer[m2] = (byte) (scr_14_00_new >> 5); // 11111111 buffer[m1] = (byte) ((buffer[m1] & 7) + ((scr_14_00_new << 3) & 248)); // 11111000 // Debug //LOGGER.trace("Ditlew - SCR "+scr+" ("+(int)(scr/90000)+") -> "+scr_new+" ("+(int)(scr_new/90000)+") "+offset_sec+" secs"); } } // Ditlew - Modify GOP @SuppressWarnings("unused") private void shiftGOPByTimeSeek(int buffer_index, int offset_sec) { int m7 = modulo(buffer_index - 7, buffer.length); int m6 = modulo(buffer_index - 6, buffer.length); int m5 = modulo(buffer_index - 5, buffer.length); int m4 = modulo(buffer_index - 4, buffer.length); int m3 = modulo(buffer_index - 3, buffer.length); int m2 = modulo(buffer_index - 2, buffer.length); int m1 = modulo(buffer_index - 1, buffer.length); int m0 = modulo(buffer_index, buffer.length); // check if valid gop if (buffer[m7] == 0 && buffer[m6] == 0 && buffer[m5] == 1 && buffer[m4] == -72 && // 0xB8 - Java/PMS wants -72 // control bits ((buffer[m2] & 0x08) == 0x08) && ((buffer[m0] & 31) == 0) && // of interest !((buffer[m3] & 128) == 128) && // not drop frm !((buffer[m0] & 16) == 16) // not broken ) { // org timecode byte h = (byte) ((buffer[m3] & 124) >> 2); byte m = (byte) (((buffer[m3] & 3) << 4) + ((buffer[m2] & 240) >> 4)); byte s = (byte) (((buffer[m2] & 7) << 3) + ((buffer[m1] & 224) >> 5)); // updated offset int _offset = s + m * 60 + h * 60 + offset_sec; // new timecode byte _h = (byte) ((_offset / 3600) % 24); byte _m = (byte) ((_offset / 60) % 60); byte _s = (byte) (_offset % 60); // update gop // h - ok buffer[m3] = (byte) ((buffer[m3] & 131) + (_h << 2)); // 10000011 // m - ok buffer[m3] = (byte) ((buffer[m3] & 252) + (_m >> 4)); // 11111100 buffer[m2] = (byte) ((buffer[m2] & 15) + (_m << 4)); // 00001111 // s - ok buffer[m2] = (byte) ((buffer[m2] & 248) + (_s >> 3)); // 11111000 buffer[m1] = (byte) ((buffer[m1] & 31) + (_s << 5)); // 00011111 // Debug //LOGGER.trace("Ditlew - GOP "+h+":"+m+":"+s+" -> "+_h+":"+_m+":"+_s+" "+offset_sec+" secs"); } } private void shiftByTimeSeek(int mb, boolean mod) { shiftVideo(mb, mod); shiftAudio(mb, mod); } private boolean shiftAudio(int mb, boolean mod) { boolean bb = ( !mod && ( buffer[mb - 10] == -67 || buffer[mb - 10] == -64 ) && buffer[mb - 11] == 1 && buffer[mb - 12] == 0 && buffer[mb - 13] == 0 && (buffer[mb - 6] & 128) == 128 ) || ( mod && ( buffer[modulo(mb - 10, buffer.length)] == -67 || buffer[modulo(mb - 10, buffer.length)] == -64 ) && buffer[modulo(mb - 11, buffer.length)] == 1 && buffer[modulo(mb - 12, buffer.length)] == 0 && buffer[modulo(mb - 13, buffer.length)] == 0 && (buffer[modulo(mb - 6, buffer.length)] & 128) == 128 ); if (bb) { int pts = (((((buffer[modulo(mb - 3, buffer.length)] & 0xff) << 8) + (buffer[modulo(mb - 2, buffer.length)] & 0xff)) >> 1) << 15) + ((((buffer[modulo(mb - 1, buffer.length)] & 0xff) << 8) + (buffer[modulo(mb, buffer.length)] & 0xff)) >> 1); pts += (int) (timeseek * 90000); setTS(pts, mb, mod); return true; } return false; } private boolean shiftVideo(int mb, boolean mod) { boolean bb = (!mod && (buffer[mb - 15] == -32 || buffer[mb - 15] == -3) && buffer[mb - 16] == 1 && buffer[mb - 17] == 0 && buffer[mb - 18] == 0 && (buffer[mb - 11] & 128) == 128 && (buffer[mb - 9] & 32) == 32) || (mod && (buffer[modulo(mb - 15, buffer.length)] == -32 || buffer[modulo(mb - 15, buffer.length)] == -3) && buffer[modulo(mb - 16, buffer.length)] == 1 && buffer[modulo(mb - 17, buffer.length)] == 0 && buffer[modulo(mb - 18, buffer.length)] == 0 && (buffer[modulo(mb - 11, buffer.length)] & 128) == 128 && (buffer[modulo(mb - 9, buffer.length)] & 32) == 32); if (bb) { // check EO or FD (tsMuxeR) int pts = getTS(mb - 5, mod); int dts = 0; boolean dts_present = (buffer[modulo(mb - 11, buffer.length)] & 64) == 64; if (dts_present) { if ((buffer[modulo(mb - 4, buffer.length)] & 15) == 15) { dts = (((((255 - (buffer[modulo(mb - 3, buffer.length)] & 0xff)) << 8) + (255 - (buffer[modulo(mb - 2, buffer.length)] & 0xff))) >> 1) << 15) + ((((255 - (buffer[modulo(mb - 1, buffer.length)] & 0xff)) << 8) + (255 - (buffer[modulo(mb, buffer.length)] & 0xff))) >> 1); dts = -dts; } else { dts = getTS(mb, mod); } } int ts = (int) (timeseek * 90000); if (mb == 50 && writeCount < maxMemorySize) { dts--; } pts += ts; setTS(pts, mb - 5, mod); if (dts_present) { if (dts < 0) { buffer[modulo(mb - 4, buffer.length)] = 17; } dts += ts; setTS(dts, mb, mod); } return true; } return false; } private int getTS(int mb, boolean modulo) { int m3 = mb - 3; int m2 = mb - 2; int m1 = mb - 1; int m0 = mb; if (modulo) { m3 = modulo(m3, buffer.length); m2 = modulo(m2, buffer.length); m1 = modulo(m1, buffer.length); m0 = modulo(m0, buffer.length); } return (((((buffer[m3] & 0xff) << 8) + (buffer[m2] & 0xff)) >> 1) << 15) + ((((buffer[m1] & 0xff) << 8) + (buffer[m0] & 0xff)) >> 1); } private void setTS(int ts, int mb, boolean modulo) { int m3 = mb - 3; int m2 = mb - 2; int m1 = mb - 1; int m0 = mb; if (modulo) { m3 = modulo(m3, buffer.length); m2 = modulo(m2, buffer.length); m1 = modulo(m1, buffer.length); m0 = modulo(m0, buffer.length); } int pts_low = ts & 32767; int pts_high = (ts >> 15) & 32767; int pts_left_low = 1 + (pts_low << 1); int pts_left_high = 1 + (pts_high << 1); buffer[m3] = (byte) ((pts_left_high & 65280) >> 8); buffer[m2] = (byte) (pts_left_high & 255); buffer[m1] = (byte) ((pts_left_low & 65280) >> 8); buffer[m0] = (byte) (pts_left_low & 255); } @Override public int read(boolean firstRead, long readCount, byte buf[], int off, int len) { if (readCount > INITIAL_BUFFER_SIZE && readCount < maxMemorySize) { int newMargin = maxMemorySize - MARGIN_MEDIUM; if (bufferOverflowWarning != newMargin) { LOGGER.debug("Setting margin to 2Mb"); } this.bufferOverflowWarning = newMargin; } if (eof && readCount >= writeCount) { return -1; } int c = 0; int minBufferS = firstRead ? minMemorySize : secondread_minsize; while (writeCount - readCount <= minBufferS && !eof && c < 15) { if (c == 0) { LOGGER.trace("Suspend Read: readCount=" + readCount + " / writeCount=" + writeCount); } c++; try { Thread.sleep(CHECK_INTERVAL); } catch (InterruptedException e) { } } if (attachedThread != null) { attachedThread.setReadyToStop(false); } if (c > 0) { LOGGER.trace("Resume Read: readCount=" + readCount + " / writeCount=" + writeCount); } if (buffer == null || !buffered) { return -1; } int mb = (int) (readCount % maxMemorySize); int endOF = buffer.length; int cut = 0; if (eof && (writeCount - readCount) < len) { cut = (int) (len - (writeCount - readCount)); if (cut < 0) { cut = 0; } } if (mb >= endOF - len) { try { System.arraycopy(buffer, mb, buf, off, endOF - mb - cut); } catch (ArrayIndexOutOfBoundsException e) { LOGGER.trace("Something went wrong with the buffer, error: " + e); LOGGER.trace("buffer: " + Arrays.toString(buffer)); LOGGER.trace("mb: " + mb); LOGGER.trace("buf: " + Arrays.toString(buf)); LOGGER.trace("off: " + off); LOGGER.trace("endOF - mb - cut: " + (endOF - mb - cut)); } return endOF - mb; } else { System.arraycopy(buffer, mb, buf, off, len - cut); return len; } } @Override public int read(boolean firstRead, long readCount) { if (readCount > INITIAL_BUFFER_SIZE && readCount < maxMemorySize) { int newMargin = maxMemorySize - MARGIN_MEDIUM; if (bufferOverflowWarning != newMargin) { LOGGER.debug("Setting margin to 2Mb"); } this.bufferOverflowWarning = newMargin; } if (eof && readCount >= writeCount) { return -1; } int c = 0; int minBufferS = firstRead ? minMemorySize : secondread_minsize; while (writeCount - readCount <= minBufferS && !eof && c < 15) { if (c == 0) { LOGGER.trace("Suspend Read: readCount=" + readCount + " / writeCount=" + writeCount); } c++; try { Thread.sleep(CHECK_INTERVAL); } catch (InterruptedException e) { } } if (attachedThread != null) { attachedThread.setReadyToStop(false); } if (c > 0) { LOGGER.trace("Resume Read: readCount=" + readCount + " / writeCount=" + writeCount); } if (buffer == null || !buffered) { return -1; } try { return 0xff & buffer[(int) (readCount % maxMemorySize)]; } catch (ArrayIndexOutOfBoundsException e) { LOGGER.info("Buffer read ArrayIndexOutOfBoundsException error:"); LOGGER.info("readCount: \"" + readCount + "\""); LOGGER.info("maxMemorySize: \"" + maxMemorySize + "\""); return -1; } } @Override public synchronized void attachThread(ProcessWrapper thread) { if (attachedThread != null) { throw new RuntimeException("BufferedOutputFile is already attached to a Thread: " + attachedThread); } LOGGER.debug("Attaching thread: " + thread); attachedThread = thread; startTimer(); } private void startTimer() { if (!hidebuffer && maxMemorySize > (15 * 1048576)) { timer = new Timer(attachedThread + "-Timer"); timer.schedule(new TimerTask() { @Override public void run() { long rc = 0; if (getCurrentInputStream() != null) { rc = getCurrentInputStream().getReadCount(); PMS.get().getFrame().setReadValue(rc, ""); } long space = (writeCount - rc); LOGGER.trace("buffered: " + formatter.format(space) + " bytes / inputs: " + inputStreams.size()); // There are 1048576 bytes in a megabyte long bufferInMBs = space / 1048576; if (renderer != null) { renderer.setBuffer(bufferInMBs); } PMS.get().getFrame().updateBuffer(); } }, 0, 2000); } } @Override public void removeInputStream(WaitBufferedInputStream inputStream) { inputStreams.remove(inputStream); } @Override public void detachInputStream() { if (!hidebuffer) { PMS.get().getFrame().setReadValue(0, ""); } if (attachedThread != null) { attachedThread.setReadyToStop(true); } Runnable checkEnd = new Runnable() { @Override public void run() { try { Thread.sleep(CHECK_END_OF_PROCESS); } catch (InterruptedException e) { LOGGER.error(null, e); } if (attachedThread != null && attachedThread.isReadyToStop()) { if (!attachedThread.isDestroyed()) { attachedThread.stopProcess(); } reset(); } } }; new Thread(checkEnd, attachedThread + "-Cleanup").start(); } @Override public synchronized void reset() { if (debugOutput != null) { try { debugOutput.close(); } catch (IOException e) { LOGGER.debug("Caught exception", e); } } if (timer != null) { timer.cancel(); } if (buffer != null) { LOGGER.trace("Destroying buffer"); buffer = null; } buffered = false; if (renderer != null) { renderer.setBuffer(0); } if (!hidebuffer && maxMemorySize != 1048576) { PMS.get().getFrame().updateBuffer(); } } }