<NME> FileDownloader.java
<BEF> /*
 * Copyright 2015 The SageTV Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package sage;

import sage.io.BufferedSageFile;
import sage.io.LocalSageFile;
import java.io.IOException;
import java.net.URI;

public class FileDownloader extends SystemTask
{
  private static final int MP4_RESEEK_PREROLL_TIME = 1000;
  private static final boolean CIRC_DOWNLOAD_DEBUG = false;
  private static java.util.Map uiXferMap = new java.util.WeakHashMap();
  private static java.util.Map fileMap = new java.util.HashMap();
  // This stores the status results from file download operations....in a way this is a memory leak since it'll grow without bound;
  // but that growth should be very minimal and not cause any overall memory issue (similar to MetaImage caching all URL images forever)
  private static java.util.Map resultMap = new java.util.HashMap();
  public static FileDownloader getFileDownloader(UIManager uiMgr)
  {
    synchronized (uiXferMap)
    {
      if (uiXferMap.containsKey(uiMgr))
        return (FileDownloader) uiXferMap.get(uiMgr);
      FileDownloader rv = new FileDownloader(uiMgr);
      uiXferMap.put(uiMgr, rv);
      return rv;
    }
  }

  public FileDownloader(UIManager inUIMgr)
  {
    if (inUIMgr != null)
      uiMgrWeak = new java.lang.ref.WeakReference(inUIMgr);
  }

  public static FileDownloader getFileDownloader(java.io.File testFile)
  {
    synchronized (fileMap)
    {
      FileDownloader fd = (FileDownloader) fileMap.get(testFile);
      if (fd != null && !fd.isComplete())
        return fd;
    }
    return null;
  }

  public static Object getBGDownloadResult(java.io.File testFile)
  {
    return resultMap.get(testFile);
  }

  public static boolean isDownloading(java.io.File testFile)
  {
    return isDownloading(null, testFile);
  }
  public static boolean isDownloading(UIManager uiMgr, java.io.File testFile)
  {
    if (uiMgr == null)
    {
      return getFileDownloader(testFile) != null;
    }
    synchronized (uiXferMap)
    {
      FileDownloader fd = getFileDownloader(uiMgr);
      return (fd.myDestFile != null && fd.myDestFile.equals(testFile) && !fd.isComplete());
    }
  }

  public void cancel()
  {
    abort = true;
    if (taskThread != null)
    {
      if (Sage.DBG) System.out.println("Waiting for download thread to terminate...");
      try
      {
        // Forcibly close the HTTP connection if it's there so we don't worry about timeout issues
        if (urlinStream != null)
          urlinStream.close();
      }
      catch (Exception e){}
      try
      {
        taskThread.join(30000);
      }
      catch (InterruptedException e)
      {}
      if (Sage.DBG) System.out.println("Download thread has terminated.");
    }
  }

  public synchronized Object downloadCircularFile(String serverName, String srcFile, java.io.File destFile)
  {
    return downloadFile(serverName, srcFile, destFile, true);
  }
  // serverName can be null which means use the preferredServer, srcFile must be a file, it's downloaded to destFile which must also be a file
  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile)
  {
    return downloadFile(serverName, srcFile, destFile, false);
  }
  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular)
  {
    return downloadFile(serverName, srcFile, destFile, circular, null);
  }
  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular, java.util.Properties requestProps)
  {
    return downloadFile(serverName, srcFile, destFile, circular, requestProps, false);
  }
  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular, java.util.Properties requestProps, boolean inSynchronous)
  {
    return downloadFile(serverName, srcFile, destFile, circular, requestProps, inSynchronous, false);
  }
  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular, java.util.Properties requestProps, boolean inSynchronous,
      boolean captureMode)
  {
    return downloadFile(serverName, srcFile, destFile, circular, requestProps, inSynchronous, captureMode, null);
  }
  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular, java.util.Properties requestProps, boolean inSynchronous,
      boolean captureMode, DecryptInfo cryptoSpec)
  {
    this.captureMode = captureMode;
    this.cryptoInfo = cryptoSpec;
    synchronous = inSynchronous;
    UIManager uiMgr = (uiMgrWeak != null) ? ((UIManager) uiMgrWeak.get()) : null;
    success = false;
    if (circular && !Sage.client)
      circSize = Sage.getLong("downloader/circular_file_size", 8*1024*1024);
    else
      circSize = 0;
    indexAtEndOfMP4 = fullDownloadRequired = httpRangeSupported= false;
    gotSMBAccess = gotMSAccess = false;
    downloadedBytes = lastDownloadOffset = 0;
    lastPlayerRequestTimestamp = lastSeekNotifyTimestamp = 0;
    lastNotifyReadOffset = 0;
    lastDownloadTimestamp = 0;
    isMP4Stream = false;
    statusMessage = "";
    if (Sage.DBG) System.out.println("Download requested for files server=" + serverName + " src=" + srcFile + " dest=" + destFile);
    if (Sage.client && serverName == null)
      serverName = Sage.preferredServer;
    remoteUIXfer = uiMgr != null && uiMgr.getUIClientType() == UIClient.REMOTE_UI && uiMgr.hasRemoteFSSupport();
    if (serverName != null && (serverName.startsWith("http:") || serverName.startsWith("https:") || serverName.startsWith("ftp:") || serverName.startsWith("file:")))
      remoteUIXfer = false;
    if (serverName == null && !remoteUIXfer)
      return Boolean.FALSE;

    myServerName = serverName;
    if (myServerName != null && myServerName.startsWith("smb://"))
    {
      srcFile = FSManager.getInstance().requestLocalSMBAccess(myServerName);
      if (srcFile == null)
        return Boolean.FALSE;
      gotSMBAccess = true;
    }
    if (myServerName!=null && myServerName.startsWith("file:"))
      mySrcFile = new java.io.File(URI.create(myServerName));
    else
      mySrcFile = (srcFile == null) ? null : new java.io.File(srcFile);

    myDestFile = destFile;
    if (!remoteUIXfer)
    {
      java.io.File destParent = myDestFile.getParentFile();
      if (destParent != null)
        IOUtils.safemkdirs(destParent);
    }

    if (myServerName != null && (myServerName.startsWith("http:") || myServerName.startsWith("https:") || myServerName.startsWith("ftp:")))
    {
      java.net.HttpURLConnection.setFollowRedirects(true);
      try
            myURLConn.setRequestProperty("Range", "bytes=0-");
          if (Sage.getBoolean("use_sagetv_user_agent", false))
            myURLConn.setRequestProperty("User-Agent", "SageTV/" + sage.Version.MAJOR_VERSION + "." + sage.Version.MINOR_VERSION + "." + sage.Version.MICRO_VERSION);
          if (requestProps != null)
          {
            java.util.Iterator walker = requestProps.entrySet().iterator();
          if (httpRangeSupported)
            myURLConn.setRequestProperty("Range", "bytes=0-");
          if (Sage.getBoolean("use_sagetv_user_agent", false))
            myURLConn.setRequestProperty("User-Agent", "SageTV/" + sage.Version.MAJOR_VERSION + "." + sage.Version.MINOR_VERSION + "." + sage.Version.MICRO_VERSION);
          if (!"".equals(Sage.get("custom_user_agent", "")))
            myURLConn.setRequestProperty("User-Agent", Sage.get("custom_user_agent", ""));
          if (requestProps != null)
          {
            java.util.Iterator walker = requestProps.entrySet().iterator();
            while (walker.hasNext())
            {
              java.util.Map.Entry currEnt = (java.util.Map.Entry) walker.next();
              if (currEnt.getKey() != null && currEnt.getValue() != null)
                myURLConn.setRequestProperty(currEnt.getKey().toString(), currEnt.getValue().toString());
            }
          }
          try
          {
            myURLConn.setConnectTimeout(30000);
            myURLConn.setReadTimeout(30000);
          }catch (Throwable tr){} // in case its a bad JRE version w/ out these calls
          try
          {
            urlinStream = myURLConn.getInputStream();
          }
          catch (Exception e1)
          {
            // Check to see if HTTP Range requests are supported from this server
            if (myURLConn instanceof java.net.HttpURLConnection && httpRangeSupported)
            {
              java.net.HttpURLConnection httpConn = (java.net.HttpURLConnection) myURLConn;
              // 416 is the proper response for denying range requests; but some servers just send
              // a generic 400 response (like last.fm)
              if (httpConn.getResponseCode() / 100 == 4)
              {
                if (Sage.DBG) System.out.println("HTTP Range requests are NOT supported by this server. errcode=" + httpConn.getResponseCode());
                httpRangeSupported = false;
                continue;
              }
            }
            throw e1;
          }
          remoteSize = myURLConn.getContentLength();
          if (Sage.DBG) System.out.println("Download remoteSize=" + remoteSize);
          if (myURLConn instanceof java.net.HttpURLConnection)
          {
            java.net.HttpURLConnection httpConn = (java.net.HttpURLConnection) myURLConn;
            if (httpConn.getResponseCode() / 100 == 3)
            {
              if (Sage.DBG) System.out.println("Internally processing HTTP redirect...");
              urlinStream.close();
              myURL = new java.net.URL(httpConn.getHeaderField("Location"));
              continue;
            }
          }
          break;
        }
        fileOut = new BufferedSageFile(new LocalSageFile(myDestFile, false));
        if (captureMode)
          fileOut.seek(fileOut.length());
        else
          fileOut.setLength(0);
      }
      catch (Exception e)
      {
        System.out.println("ERROR with file download of:" + e);
        try{
          if (urlinStream != null)
            urlinStream.close();
        }catch (Exception e3){}
        try{
          if (fileOut != null)
            fileOut.close();
        }catch (Exception e4){}
        cleanup();
        return Boolean.FALSE;
      }
    }
    else if (remoteUIXfer)
    {
      if (captureMode)
      {
        // capture mode not supported for remote UI xfers
        return Boolean.FALSE;
      }
      if (!mySrcFile.isFile())
      {
        cleanup();
        return Boolean.FALSE;
      }

      MiniClientSageRenderer mcsr = (MiniClientSageRenderer) uiMgr.getRootPanel().getRenderEngine();
      remoteSize = mySrcFile.length();
      MiniClientSageRenderer.RemoteFSXfer fsXferOp = mcsr.fsDownloadFile(mySrcFile, destFile.toString());
      if (fsXferOp.error != 0)
      {
        cleanup();
        return Boolean.FALSE;
      }
      fsXferOpWeak = new java.lang.ref.WeakReference(fsXferOp);
    }
    else if (myServerName!=null && mySrcFile!=null && mySrcFile.isFile() && myServerName.startsWith("file:"))
    {
      if (Sage.DBG) System.out.println("FileDownloader: Local File Copy: From: " + mySrcFile + " to " + destFile);
      // file url passed as the url to download
      if (!mySrcFile.exists())
      {
        cleanup();
        return Boolean.FALSE;
      }
    }
    else
    {
      try
      {
        sock = new java.net.Socket();
        sock.connect(new java.net.InetSocketAddress(Sage.preferredServer, 7818), 5000);
        sock.setSoTimeout(30000);
        //sock.setTcpNoDelay(true);
        outStream = new java.io.DataOutputStream(new java.io.BufferedOutputStream(sock.getOutputStream()));
        inStream = new java.io.DataInputStream(new java.io.BufferedInputStream(sock.getInputStream()));
        outStream.write("OPENW ".getBytes(Sage.BYTE_CHARSET));
        outStream.write(mySrcFile.toString().getBytes("UTF-16BE"));
        outStream.write("\r\n".getBytes(Sage.BYTE_CHARSET));
        outStream.flush();
        String str = Sage.readLineBytes(inStream);
        if ("NON_MEDIA".equals(str))
        {
          // We may need to get special access to read this file; so try that first
          if (NetworkClient.getSN().requestMediaServerAccess(mySrcFile, true))
          {
            gotMSAccess = true;
            outStream.write("OPENW ".getBytes(Sage.BYTE_CHARSET));
            outStream.write(mySrcFile.toString().getBytes("UTF-16BE"));
            outStream.write("\r\n".getBytes(Sage.BYTE_CHARSET));
            outStream.flush();
            str = Sage.readLineBytes(inStream);
          }
        }
        if (!"OK".equals(str))
          throw new java.io.IOException("Error opening remote file of:" + str);
        // get the size
        outStream.write("SIZE\r\n".getBytes(Sage.BYTE_CHARSET));
        outStream.flush();
        str = Sage.readLineBytes(inStream);
        remoteSize = Long.parseLong(str.substring(0, str.indexOf(' ')));
        fileOut = new BufferedSageFile(new LocalSageFile(myDestFile, false));
        if (captureMode)
          fileOut.seek(fileOut.length());
        else
          fileOut.setLength(0);
      }
      catch (Exception e)
      {
        System.out.println("ERROR with file download of:" + e);
        try{
          if (sock != null)
            sock.close();
        }catch (Exception e1){}
        try{
          if (outStream != null)
            outStream.close();
        }catch (Exception e2){}
        try{
          if (inStream != null)
            inStream.close();
        }catch (Exception e3){}
        try{
          if (fileOut != null)
            fileOut.close();
        }catch (Exception e4){}
        cleanup();
        return Boolean.FALSE;
      }
    }
    // Don't allow others to look at our files if this was done in appendMode because it won't give proper information
    if (!captureMode)
    {
      synchronized (fileMap)
      {
        fileMap.put(myDestFile, this);
      }
    }
    startTaskThread("FileDownload", synchronous);

    return (synchronous && !success) ? Boolean.FALSE : Boolean.TRUE;
  }

  public void taskRun()
  {
    try
    {
      if (myServerName != null && (myServerName.startsWith("http:") || myServerName.startsWith("https:") || myServerName.startsWith("ftp:")))
      {
        httpTaskRun();
      }
      else if (remoteUIXfer)
      {
        remoteUITaskRun();
      }
      else if (myServerName!=null && myServerName.startsWith("file:"))
      {
        fileTaskRun();
      }
      else
      {
        stvTaskRun();
      }
      if (Sage.DBG) System.out.println("Download completed for files server=" + myServerName + " src=" + mySrcFile + " dest=" + myDestFile + " abort=" + abort);
    }
    finally
    {
      cleanup();
      if (uiMgrWeak == null)
      {
        resultMap.put(myDestFile, wasSuccessful() ? (Object)Boolean.TRUE : (Object)("Error: " + getStatusMessage()));
      }
      synchronized (fileMap)
      {
        fileMap.remove(myDestFile);
      }
    }
  }

  private void fileTaskRun() {
    try {
      downloadedBytes=myDestFile.length();
      statusMessage="100%";

      if (mySrcFile.equals(myDestFile)) {
        succeeded();
        return;
      }

      IOUtils.copyFile(mySrcFile, myDestFile);
      succeeded();
    }
    catch (IOException e)
    {
      if (Sage.DBG) System.out.println("ERROR during file download/copy of:" + e);
      Sage.printStackTrace(e);
      statusMessage = "Error:" + e.toString();
    }
  }

  private void cleanup()
  {
    if (gotMSAccess)
    {
      NetworkClient.getSN().requestMediaServerAccess(mySrcFile, false);
      gotMSAccess = false;
    }
    if (gotSMBAccess)
    {
      FSManager.getInstance().releaseLocalSMBAccess(myServerName);
      gotSMBAccess = false;
    }

  }

  // NOTE: Not all servers support Range download requests!!!!
  private void restartHttpDownload(long startOffset) throws java.io.IOException
  {
    if (startOffset > 0 && !httpRangeSupported)
      throw new java.io.IOException("Cannot restart HTTP download because we already know this server doesn't support Range! offset=" + startOffset);
    // Close current connection first
    try{
      if (urlinStream != null)
        urlinStream.close();
    }catch (Exception e3){}
    myURLConn = myURL.openConnection();
    if (httpRangeSupported)
      myURLConn.setRequestProperty("Range", "bytes=" + startOffset + "-");
    try
    {
      myURLConn.setConnectTimeout(30000);
      myURLConn.setReadTimeout(30000);
    }catch (Throwable tr){} // in case its a bad JRE version w/ out these calls
    urlinStream = myURLConn.getInputStream();
    if (myURLConn instanceof java.net.HttpURLConnection)
    {
      java.net.HttpURLConnection httpConn = (java.net.HttpURLConnection) myURLConn;
      if (httpConn.getResponseCode() / 100 == 3)
      {
        if (Sage.DBG) System.out.println("Internally processing HTTP redirect to: " + httpConn.getHeaderField("Location") +
            " responseCode=" + httpConn.getResponseCode());
        urlinStream.close();
        myURL = new java.net.URL(httpConn.getHeaderField("Location"));
        if (abort) return;
        restartHttpDownload(startOffset);
        return;
      }
    }
    lastDownloadOffset = downloadedBytes = startOffset;
    //		lastSeekNotifyTimestamp = 0; // reset this indicator
    if (circSize > 0)
    {
      synchronized (notifyRead)
      {
        // Reposition this file
        if (fileOut.length() < (startOffset % circSize))
          fileOut.setLength(startOffset % circSize);
        fileOut.seek(startOffset % circSize);
        notifyRead.notify();
      }
    }
    if (Sage.DBG) System.out.println("HTTP Download has been restarted at offset=" + startOffset);
  }

  // This'll reconnect to the server if it needs to
  private int safeHttpRead(byte[] buffer) throws java.io.IOException
  {
    try
    {
      int rv = urlinStream.read(buffer);
      // For encrypted files, due to partial decryption we can't trust the download size check here
      if (rv <= 0 && remoteSize > 0 && downloadedBytes < remoteSize && httpRangeSupported && cryptoInfo == null)
      {
        if (Sage.DBG) System.out.println("Read " + rv + " bytes from the URL stream; but we're not done with the file yet...restart the download...");
        restartHttpDownload(downloadedBytes);
        return urlinStream.read(buffer);
      }
      return rv;
    }
    catch (java.io.IOException e)
    {
      if (httpRangeSupported && !abort)
      {
        if (Sage.DBG) System.out.println("HTTP read failed; trying to reconnect...");
        restartHttpDownload(downloadedBytes);
        return urlinStream.read(buffer);
      }
      else
        throw e;
    }
  }

  // NOTE: We should monitor the download and resume it if it hangs for some reason....that will definitely occur
  // on large file transfers at some point or another
  private void httpTaskRun()
  {
    downloadedBytes = 0;
    if (xferBuf == null)
      xferBuf = new byte[Sage.getInt("file_transfer_buffer_size", 65536)];
    java.text.NumberFormat prctForm = java.text.NumberFormat.getPercentInstance();
    if (abort)
    {
      // In case someone is viewing while we're downloading
      Sage.processInactiveFile(myDestFile.toString());
      return;
    }
    boolean isFlashStream = false;
    boolean foundID3Tag = false;
    int id3HeaderSize = 0;
    boolean isMP3Stream = false;
    boolean isSWFStream = false;
    boolean isMpeg2PSStream = false;
    boolean isMpeg2TSStream = false;
    bytesUntilNextSizeTimestamp = 4;
    numParsedSizeTimestampBytes = 0;
    byte[] decryptBuf = null;
    try
    {
      download_loop:
        do
        {
          if (abort)
          {
            // In case someone is viewing while we're downloading
            Sage.processInactiveFile(myDestFile.toString());
            return;
          }
          if (circSize > 0)
          {
            if (lastNotifyReadOffset < Math.max(lastDownloadOffset, downloadedBytes - circSize))
            {
              if (Sage.DBG) System.out.println("DOWNLOADER restarting download to move the stream BACKWARDS notifyReadOffset=" + lastNotifyReadOffset +
                  " lastSeekTimestamp=" + lastSeekNotifyTimestamp);
              // Try to figure out where we should really seek too if we can
              long reseekTarget = getSafeMP4SeekPositionForReadPosition(lastNotifyReadOffset, MP4_RESEEK_PREROLL_TIME);
              /*						if (isMP4Stream && lastSeekNotifyTimestamp > 0)
						{
							long[] dataRange = getMinMaxByteRangeForTimestamp(lastSeekNotifyTimestamp);
							if (dataRange[0] < reseekTarget)
							{
								if (Sage.DBG) System.out.println("Adjusting the backwards request to get all streams in it....was=" +
									reseekTarget + " now=" + dataRange[0]);
								reseekTarget = dataRange[0];
							}
						}*/
              if (reseekTarget < Math.max(lastDownloadOffset, downloadedBytes - circSize))
              {
                bytesUntilNextAtom += downloadedBytes - reseekTarget; // if we're doing the initial MP4 parsing this is needed
                restartHttpDownload(reseekTarget);
                lastAutoReseekTime = Sage.time();
              }
              else if (Sage.DBG) System.out.println("Skipping re-seek since it's target is within our download buffer " + reseekTarget);
            }
            // Check to see if this read request is for data that's after what's in the buffer
            else if (isMP4Stream && lastNotifyReadOffset > downloadedBytes + 256*1024)
              /*					else if (isMP4Stream && (lastNotifyReadOffset > downloadedBytes + circSize ||
						(lastSeekNotifyTimestamp != 0 && lastNotifyReadOffset > downloadedBytes + 128*1024)))*/
              //						((Sage.time() - lastAutoReseekTime) < 5000 ? (circSize*8/10) : 512*1024))
            {
              // Only skip MP4 streams ahead if it's more than the circular buffer (which is definitely a skip)
              // or if the timestamp seek hint has been set as well...otherwise this is probably just another read
              if (Sage.DBG) System.out.println("DOWNLOADER restarting download to move the stream FORWARDS notifyOffset=" + lastNotifyReadOffset +
                  " lastSeekTimestamp=" + lastSeekNotifyTimestamp);
              long reseekTarget = getSafeMP4SeekPositionForReadPosition(lastNotifyReadOffset, MP4_RESEEK_PREROLL_TIME);
              /*						if (lastSeekNotifyTimestamp > 0)
						{
							long[] dataRange = getMinMaxByteRangeForTimestamp(lastSeekNotifyTimestamp);
							if (dataRange[0] < reseekTarget)
							{
								if (Sage.DBG) System.out.println("Adjusting the forwards request to get all streams in it....was=" +
									reseekTarget + " now=" + dataRange[0]);
								reseekTarget = dataRange[0];
							}
						}*/
              if (reseekTarget > downloadedBytes)
              {
                bytesUntilNextAtom -= reseekTarget - downloadedBytes; // if we're doing the initial MP4 parsing this is needed
                restartHttpDownload(reseekTarget);
                lastAutoReseekTime = Sage.time();
              }
              else if (Sage.DBG) System.out.println("Skipping re-seek since it's target is within our download buffer " + reseekTarget);
            }
          }
          if (decryptBuf != null)
          {
            byte[] tempBuf = xferBuf;
            // Swap the buffers back
            xferBuf = decryptBuf;
            decryptBuf = tempBuf;
          }
          int numRead = safeHttpRead(xferBuf);
          if (numRead <= 0)
          {
            if (circSize > 0 && isMP4Stream && httpRangeSupported && indexAtEndOfMP4 && !abort)
            {
              if (Sage.DBG) System.out.println("Waiting at the end of the MP4 stream since we're probably just reading the header...");
              fileOut.flush();
            }
            while (circSize > 0 && isMP4Stream && httpRangeSupported && indexAtEndOfMP4 && !abort)
            {
              if (lastNotifyReadOffset < Math.max(lastDownloadOffset, downloadedBytes - circSize))
              {
                continue download_loop;
              }
              synchronized (notifyRead)
              {
                try
                {
                  notifyRead.wait(30);
                }
                catch (InterruptedException e2){}
              }
            }
            if (Sage.DBG) System.out.println("Read " + numRead + " bytes from the URL stream; download has completed");
            if (cryptoInfo != null)
            {
              numRead = decryptBuffer(xferBuf, 0, decryptBuf, true);
              if (numRead > 0)
              {
                if (circSize > 0)
                {
                  waitForCircWrite(numRead);
                  synchronized (notifyRead)
                  {
                    if (fileOut.position() + numRead < circSize)
                      fileOut.write(decryptBuf, 0, numRead);
                    else
                    {
                      int firstWrite = (int)(circSize - fileOut.position());
                      fileOut.write(decryptBuf, 0, firstWrite);
                      fileOut.seek(0);
                      fileOut.write(decryptBuf, firstWrite, numRead - firstWrite);
                    }
                  }
                }
                else
                  fileOut.write(decryptBuf, 0, numRead);
              }
            }
            break;
          }
          if (cryptoInfo != null)
          {
            if (decryptBuf == null)
              decryptBuf = new byte[xferBuf.length*3/2]; // make it 50% bigger to handle the final buffer which may be larger
            numRead = decryptBuffer(xferBuf, numRead, decryptBuf, false);
            byte[] tempBuf = xferBuf;
            // Swap the buffers now, then do it again before we download more data
            xferBuf = decryptBuf;
            decryptBuf = tempBuf;
          }
          if (circSize > 0)
          {
            waitForCircWrite(numRead);
            synchronized (notifyRead)
            {
              if (fileOut.position() + numRead < circSize)
                fileOut.write(xferBuf, 0, numRead);
              else
              {
                int firstWrite = (int)(circSize - fileOut.position());
                fileOut.write(xferBuf, 0, firstWrite);
                fileOut.seek(0);
                fileOut.write(xferBuf, firstWrite, numRead - firstWrite);
              }
            }
          }
          else
            fileOut.write(xferBuf, 0, numRead);
          if (downloadedBytes == 0 && numRead >= 10 && !isFlashStream && !isMP3Stream && !isMP4Stream && !isSWFStream && !isMpeg2PSStream && !isMpeg2TSStream)
          {
            // Check if it's a flash stream
            if (xferBuf[0] == 'F' && xferBuf[1] == 'L' && xferBuf[2] == 'V' && (((xferBuf[4] & 0x1) == 1) || ((xferBuf[4] & 0x4) == 4)))
            {
              if (Sage.DBG) System.out.println("Detected FLV stream downloading...");
              isFlashStream = true;
              processFlashPacket(xferBuf, 9, numRead - 9);
            }
            else if (xferBuf[0] == 'I' && xferBuf[1] == 'D' && xferBuf[2] == '3')
            {
              if (Sage.DBG) System.out.println("Detected ID3 tag in downloading...");
              foundID3Tag = true;
              id3HeaderSize = ((xferBuf[6]&0x7F)<<21) | ((xferBuf[7]&0x7F)<<14) |
                  ((xferBuf[8]&0x7F)<<7) | (xferBuf[9]&0x7F);
              id3HeaderSize += 10; // for the 10 initial bytes
              isMP3Stream = true; // assume ID3 is MP3 for now
            }
            else if (xferBuf[4] == 'f' && xferBuf[5] == 't' && xferBuf[6] == 'y' && xferBuf[7] == 'p')
            {
              // NOTE: This check could be much more solid....but this should do I'm pretty sure. :)
              if (Sage.DBG) System.out.println("Detected MP4 ISO stream downloading...");
              isMP4Stream = true;
              initMP4Parsing();
              processMP4Packet(xferBuf, 0, numRead);
            }
            else if (xferBuf[4] == 'm' && xferBuf[5] == 'd' && xferBuf[6] == 'a' && xferBuf[7] == 't')
            {
              // Handle quicktime files like MP4s for now....
              if (Sage.DBG) System.out.println("Detected Quicktime stream downloading...");
              isMP4Stream = true;
              initMP4Parsing();
              processMP4Packet(xferBuf, 0, numRead);
            }
            else if ((xferBuf[0] == 'C' || xferBuf[0] == 'F') && xferBuf[1] == 'W' && xferBuf[2] == 'S')
            {
              if (Sage.DBG) System.out.println("Detected SWF stream downloading...");
              isSWFStream = true;
            }
            else if (xferBuf[0] == 0 && xferBuf[1] == 0 && xferBuf[2] == 1 && (xferBuf[3] & 0xFF) == 0xBA)
            {
              if (Sage.DBG) System.out.println("Detected MPEG2-PS stream downloading...");
              isMpeg2PSStream = true;
            }
            else if (xferBuf[0] == 0x47 && (numRead < 189 || xferBuf[188] == 0x47) && (numRead < 377 || xferBuf[376] == 0x47))
            {
              if (Sage.DBG) System.out.println("Detected MPEG2-TS stream downloading...");
              isMpeg2TSStream = true;
            }
            else if (processMP3Packet(xferBuf, 0, numRead))
            {
              if (Sage.DBG) System.out.println("Detected MPEG audio stream downloading...");
              isMP3Stream = true;
            }
          }
          else if (isFlashStream)
            processFlashPacket(xferBuf, 0, numRead);
          else if (isMP4Stream)
            processMP4Packet(xferBuf, 0, numRead);
          else if (isMP3Stream && mp3Bitrate == 0)
          {
            processMP3Packet(xferBuf, 0, numRead);
          }
          else if (isMpeg2PSStream && !captureMode)
            processMpeg2PSPacket(xferBuf, 0, numRead);
          else if (isMpeg2TSStream && !captureMode)
            processMpeg2TSPacket(xferBuf, 0, numRead);
          if (downloadedBytes == 0)
          {
            // So we don't have a zero length file on disk which'll fail verification
            fileOut.flush();
          }
          downloadedBytes += numRead;
          if (CIRC_DOWNLOAD_DEBUG) System.out.println("DownloadedBytes=" + downloadedBytes);
          if (isMP3Stream && downloadedBytes > id3HeaderSize && mp3Bitrate > 0)
          {
            // Estimate the MP3 time that we've downloaded (milliseconds)
            lastDownloadTimestamp = (downloadedBytes - id3HeaderSize)*8000 / mp3Bitrate;
          }
          if (remoteSize > 0)
            statusMessage = prctForm.format(((double)downloadedBytes)/remoteSize);
          else
            statusMessage = (downloadedBytes/1024) + " KB";
        }while (true);
    }
    catch (Exception e)
    {
      if (Sage.DBG) System.out.println("ERROR during file download of:" + e);
      Sage.printStackTrace(e);
      statusMessage = "Error:" + e.toString();
      return;
    }
    finally
    {
      try{
        if (urlinStream != null)
          urlinStream.close();
      }catch (Exception e3){}
      try{
        if (fileOut != null)
          fileOut.close();
      }catch (Exception e4){}
      cleanupMP4Parsing();
    }

    succeeded();
    // In case someone is viewing while we're downloading
    if (!captureMode)
      Sage.processInactiveFile(myDestFile.toString());
  }


  // We have the type+size+timestamp(1+3+3)
  private int bytesUntilNextSizeTimestamp = 4; // 4 byte offset into the first packet
  private int numParsedSizeTimestampBytes = 0;
  private byte[] sizeTimestampData = new byte[7];
  private void processFlashPacket(byte[] buf, int offset, int length)
  {
    while (length > 0)
    {
      if (bytesUntilNextSizeTimestamp > 0)
      {
        if (bytesUntilNextSizeTimestamp < length)
        {
          offset += bytesUntilNextSizeTimestamp;
          length -= bytesUntilNextSizeTimestamp;
          bytesUntilNextSizeTimestamp = 0;
          continue;
        }
        else
        {
          bytesUntilNextSizeTimestamp -= length;
          return;
        }
      }
      else
      {
        int neededBytes = 7 - numParsedSizeTimestampBytes;
        int availBytes = Math.min(neededBytes, length);
        System.arraycopy(buf, offset, sizeTimestampData, numParsedSizeTimestampBytes, availBytes);
        numParsedSizeTimestampBytes += availBytes;
        offset += availBytes;
        length -= availBytes;

        if (numParsedSizeTimestampBytes == 7)
        {
          // We have a complete type+size + timestamp
          int streamType = sizeTimestampData[0];
          int tagBodyLength = ((sizeTimestampData[1] & 0xFF) << 16) + ((sizeTimestampData[2] & 0xFF) << 8) + (sizeTimestampData[3] & 0xFF);
          int tagTimestamp = ((sizeTimestampData[4] & 0xFF) << 16) + ((sizeTimestampData[5] & 0xFF) << 8) + (sizeTimestampData[6] & 0xFF);
          if (streamType == 9) // video
          {
            lastDownloadTimestamp = tagTimestamp;
          }
          numParsedSizeTimestampBytes = 0;
          bytesUntilNextSizeTimestamp = tagBodyLength + 8;
        }
      }
    }
  }

  private void processMpeg2PSPacket(byte[] buf, int offset, int length)
  {
    // We just search for the PTS's in the 0xe0 video stream and use those for the latest timestamp
    for (int i = offset; i < offset + length - 27; i++)
    {
      if (buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1 && (buf[i + 3] & 0xFF) == 0xBA)
      {
        // Pack header is here
        // Skip over the data in the pack header and land on the pack_stuffing_length byte (it uses the lower 3 bits)
        i += 13;
        // Check for any stuffing bytes in the pack header and skip them
        i += (1 + (buf[i] & 0x7));
        if (i + 13 >= offset + length) break;
        // Check for a start code next, it can be a system header or a pes packet
        while (buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1 && (buf[i + 3] & 0xFF) != 0xBA)
        {
          i += 3;
          if ((buf[i] & 0xFF) == 0xBB)
          {
            // system header, skip it
            i += 3 + (((buf[i + 1] & 0xFF) << 8) | (buf[i + 2] & 0xFF));
            // Check for the start code next
            if (i + 15 >= offset + length) break;
            if (buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 1)
              continue;
            else
              i += 3;
          }
          if (i + 15 >= offset + length) break;
          int stream_id = buf[i] & 0xFF;
          int packet_length = ((buf[i + 1] & 0xFF) << 8) | (buf[i + 2] & 0xFF);
          //System.out.println("PES stream id=0x" + Integer.toString(stream_id, 16) + " length=" + packet_length);
          if (stream_id == 0xBD || (stream_id >= 0xC0 && stream_id <= 0xEF))
          {
            //System.out.println("Audio/video substream found");
            // Valid audio or video stream, check for the pts flag
            if ((buf[i + 4] & 0x80) == 0x80)
            {
              // PTS found, extract it
              long pts = (((long)(buf[i + 6] & 0x0E)) << 29) |
                  ((buf[i + 7] & 0xFF) << 22) |
                  ((buf[i + 8] & 0xFE) << 14) |
                  ((buf[i + 9] & 0xFF) << 7) |
                  ((buf[i + 10] & 0xFE) >> 1);
              lastDownloadTimestamp = pts / 90; // convert to milliseconds
            }
          }
        }
      }
    }
  }

  private void processMpeg2TSPacket(byte[] buf, int offset, int length)
  {
    for (int i = offset; i < offset + length - 188; i++)
    {
      // Align on a TS sync code
      if (buf[i] == 0x47)
      {
        // Find a payload start indicator, that's the only way there's PTS at the head
        if ((buf[i + 1] & 0x40) != 0)
        {
          int adaptation_field_control = (buf[i + 3] & 0x30) >> 4;
    if (adaptation_field_control == 2)
      i += (188 - 4);
    else if (adaptation_field_control == 3)
      i += Math.min(188 - 6, 1 + (buf[i + 4] & 0xFF));
    i += 4;
    if (adaptation_field_control == 1 || adaptation_field_control == 3)
    {
      // Check for a start code next, it can be a system header or a pes packet
      if (buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1)
      {
        i += 3;
        if (i + 15 >= offset + length) break;
        int stream_id = buf[i] & 0xFF;
        //System.out.println("PES stream id=0x" + Integer.toString(stream_id, 16));
        if ((stream_id == 0xBD || (stream_id >= 0xC0 && stream_id <= 0xEF) || (stream_id == 0xFD)))
        {
          //System.out.println("Audio/video substream found");
          // Valid audio or video stream, check for the pts flag
          if ((buf[i + 4] & 0x80) == 0x80)
          {
            // PTS found, extract it
            long pts = (((long)(buf[i + 6] & 0x0E)) << 29) |
                ((buf[i + 7] & 0xFF) << 22) |
                ((buf[i + 8] & 0xFE) << 14) |
                ((buf[i + 9] & 0xFF) << 7) |
                ((buf[i + 10] & 0xFE) >> 1);
            lastDownloadTimestamp = pts / 90; // convert to milliseconds
          }
        }
      }
    }
        }
      }
    }
  }

  private long bytesUntilNextAtom = 0;
  private byte[] remAtomData = new byte[28];
  private int remAtomSize = 0;
  private int[] sttsDataA; // even are the counts, odd are the durations
  private int[] sttsDataV; // even are the counts, odd are the durations
  private int sttsIndex;
  private int[] stscDataA; // even are the first chunk #, odd are the samples per chunk
  private int[] stscDataV; // even are the first chunk #, odd are the samples per chunk
  private int stscIndex;
  private long[] chunkOffsetsA; // one for each chunk
  private long[] chunkOffsetsV; // one for each chunk
  private int stcoIndex;
  private boolean areOffsets64Bits;
  private int chunkQuality;
  private int timeScale;
  private long[] mp4TimeIndexA = null; // chunkOffsets has the positions, this is the same length and has the times
  private long[] mp4TimeIndexV = null; // chunkOffsets has the positions, this is the same length and has the times
  private boolean movieDataBegan;
  private long lastSeekNotifyTimestamp;
  private long lastPlayerRequestTimestamp;
  private static final boolean MP4_PARSER_DEBUG = false;
  private void initMP4Parsing()
  {
    bytesUntilNextAtom = 0;
    remAtomSize = 0;
    sttsIndex = stscIndex = stcoIndex = -1;
    chunkQuality = 0;
    movieDataBegan = false;
  }
  private void cleanupMP4Parsing()
  {
    chunkOffsetsA = chunkOffsetsV = null;
    sttsDataA = sttsDataV = stscDataA = stscDataV = null;
    mp4TimeIndexA = mp4TimeIndexV = null;
  }
  private long findTimestampForBytePos(long pos, long[] bytePos, long[] timestamps)
  {
    int idx = java.util.Arrays.binarySearch(bytePos, pos);
    if (idx < 0)
      idx = (-(idx + 1)) - 1; // find the insertion point then backup one
    if (idx < 0)
      idx = 0; // don't go before zero
    return timestamps[idx];
  }
  private long findBytePosForTimestamp(long timestamp, long[] bytePos, long[] timestamps)
  {
    int idx = java.util.Arrays.binarySearch(timestamps, timestamp);
    if (idx < 0)
      idx = (-(idx + 1)) - 1; // find the insertion point then backup one
    if (idx < 0)
      idx = 0; // don't go before zero
    return bytePos[idx];
  }
  private long[] getMinMaxByteRangeForTimestamp(long timestamp)
  {
    if (!isMP4Stream)
      return null;
    long min = Long.MAX_VALUE;
    long max = 0;
    if (chunkOffsetsA != null && mp4TimeIndexA != null)
    {
      long apos = findBytePosForTimestamp(timestamp, chunkOffsetsA, mp4TimeIndexA);
      min = Math.min(apos, min);
      max = Math.max(apos, max);
    }
    if (chunkOffsetsV != null && mp4TimeIndexV != null)
    {
      long vpos = findBytePosForTimestamp(timestamp, chunkOffsetsV, mp4TimeIndexV);
      min = Math.min(vpos, min);
      max = Math.max(vpos, max);
    }
    return new long[] { min, max };
  }
  private long getSafeMP4SeekPositionForReadPosition(long readPos, long timeOffset)
  {
    if (isMP4Stream && chunkOffsetsV != null && mp4TimeIndexV != null && mp4TimeIndexA != null &&
        chunkOffsetsA != null)
    {
      // Check to see if we think we know exactly what time they're looking for:
      int testidx = java.util.Arrays.binarySearch(chunkOffsetsA, readPos);
      long atime, vtime;
      if (testidx >= 0)
      {
        if (CIRC_DOWNLOAD_DEBUG) System.out.println("ESTIMATING-A seek time to be:" + mp4TimeIndexA[testidx]);
        atime = vtime = mp4TimeIndexA[testidx];
      }
      else if ((testidx = java.util.Arrays.binarySearch(chunkOffsetsV, readPos)) >= 0)
      {
        if (CIRC_DOWNLOAD_DEBUG) System.out.println("ESTIMATING-V seek time to be:" + mp4TimeIndexV[testidx]);
        atime = vtime = mp4TimeIndexV[testidx];
      }
      else
      {
        // Figure out what are the two potential timestamps for this read position.
        // Then take the minimum of those two values.
        // Then look up the positions for each stream for that timestamp.
        // Then use the minimum of those two positions for where we think the client is reading from currently.
        atime = findTimestampForBytePos(readPos, chunkOffsetsA, mp4TimeIndexA);
        vtime = findTimestampForBytePos(readPos, chunkOffsetsV, mp4TimeIndexV);
        long maxtime, mintime;
        if (atime > vtime)
        {
          maxtime = atime;
          mintime = vtime;
        }
        else
        {
          maxtime = vtime;
          mintime = atime;
        }
        if (CIRC_DOWNLOAD_DEBUG) System.out.println("ESTIMATING A-V seek times to be A=" + atime + " V=" + vtime);
      }
      long testTime = Math.min(atime, vtime);
      testTime = Math.max(0, testTime - timeOffset); // for keyframe alignment
      atime = findBytePosForTimestamp(testTime, chunkOffsetsA, mp4TimeIndexA);
      vtime = findBytePosForTimestamp(testTime, chunkOffsetsV, mp4TimeIndexV);
      return Math.min(readPos, Math.min(atime, vtime));
    }
    else
      return readPos;
  }

  private void processMP4Packet(byte[] buf, int offset, int length)
  {
    if (movieDataBegan)
    {
      long newMinTimestamp = Long.MAX_VALUE;
      if (!fullDownloadRequired)
      {
        if (chunkOffsetsA != null && mp4TimeIndexA != null)
        {
          newMinTimestamp = Math.min(findTimestampForBytePos(downloadedBytes, chunkOffsetsA, mp4TimeIndexA), newMinTimestamp);
        }
        if (chunkOffsetsV != null && mp4TimeIndexV != null)
        {
          newMinTimestamp = Math.min(findTimestampForBytePos(downloadedBytes, chunkOffsetsV, mp4TimeIndexV), newMinTimestamp);
        }
        if (newMinTimestamp != Long.MAX_VALUE)
          lastDownloadTimestamp = newMinTimestamp;
      }
      if (!MP4_PARSER_DEBUG && (!indexAtEndOfMP4 || (mp4TimeIndexA != null && mp4TimeIndexV != null)))
        return;
    }
    if (bytesUntilNextAtom > 0)
    {
      if (remAtomSize > 0)
      {
        int used = Math.min((int)bytesUntilNextAtom, remAtomSize);
        if (used != remAtomSize)
        {
          System.arraycopy(remAtomData, used, remAtomData, 0, remAtomSize - used);
        }
        remAtomSize -= used;
        bytesUntilNextAtom -= used;
      }
      if (bytesUntilNextAtom > 0)
      {
        long used = Math.min(bytesUntilNextAtom, length);
        length -= used;
        offset += used;
        bytesUntilNextAtom -= used;
        if (length == 0)
          return;
      }
    }

    while ((length + remAtomSize >= remAtomData.length && bytesUntilNextAtom >= 0) || (downloadedBytes + offset + length == remoteSize && length > 0))
    {
      System.arraycopy(buf, offset, remAtomData, remAtomSize, Math.min(remAtomData.length - remAtomSize, length));
      long currSize;
      if (stcoIndex >= 0)
      {
        long currVal;
        bytesUntilNextAtom = 0;
        long[] chunkOffsets = (chunkQuality == 2) ? chunkOffsetsV : chunkOffsetsA;
        if (areOffsets64Bits)
        {
          for (int x = 0; x + 7 < remAtomData.length; x+=8)
          {
            currVal = ((remAtomData[x] & 0xFF) << 56) | ((remAtomData[x+1] & 0xFF) << 48) | ((remAtomData[x+2] & 0xFF) << 40) | ((remAtomData[x+3] & 0xFF) << 32) |
                ((remAtomData[x+4] & 0xFF) << 24) | ((remAtomData[x+5] & 0xFF) << 16) | ((remAtomData[x+6] & 0xFF) << 8) | (remAtomData[x+7] & 0xFF);
            bytesUntilNextAtom += 8;
            chunkOffsets[stcoIndex++] = currVal;
            if (stcoIndex == chunkOffsets.length)
            {
              stcoIndex = -1;
              if (MP4_PARSER_DEBUG) System.out.println("Done reading chunk offset atom");
              break;
            }
          }
        }
        else
        {
          for (int x = 0; x + 3 < remAtomData.length; x+=4)
          {
            currVal = ((remAtomData[x] & 0xFF) << 24) | ((remAtomData[x+1] & 0xFF) << 16) | ((remAtomData[x+2] & 0xFF) << 8) | (remAtomData[x+3] & 0xFF);
            bytesUntilNextAtom += 4;
            chunkOffsets[stcoIndex++] = currVal;
            if (stcoIndex == chunkOffsets.length)
            {
              stcoIndex = -1;
              if (MP4_PARSER_DEBUG) System.out.println("Done reading chunk offset atom");
              break;
            }
          }
        }
        if (stcoIndex == -1)
        {
          // We're done parsing the sample information so now we can build the time-byte index
          if (Sage.DBG) System.out.println("Building MP4 byte-time index map to track downloading");
          long[] mp4TimeIndex = new long[chunkOffsets.length];
          if (chunkQuality == 2)
            mp4TimeIndexV = mp4TimeIndex;
          else
            mp4TimeIndexA = mp4TimeIndex;
          int chunkNum = 0;
          long sampleNum = 0;
          sttsIndex = 0;
          stscIndex = 0;
          long time = 0;
          long sampleDuration;
          int samplesLeftInChunk = 0;
          int[] stscData = (chunkQuality == 2) ? stscDataV : stscDataA;
          int[] sttsData = (chunkQuality == 2) ? sttsDataV : sttsDataA;
          while (true)
          {
            if (samplesLeftInChunk == 0)
            {
              mp4TimeIndex[chunkNum] = time;
              chunkNum++;
              if (chunkNum == chunkOffsets.length)
                break;
              if (stscIndex + 2 < stscData.length && chunkNum >= stscData[stscIndex + 2])
                stscIndex += 2;
              samplesLeftInChunk = stscData[stscIndex + 1];
            }
            if (sttsData[sttsIndex] == 0)
              sttsIndex+=2;
            sampleDuration = sttsData[sttsIndex + 1] * 1000 / timeScale;

            int samplesThisTime = Math.min(samplesLeftInChunk, sttsData[sttsIndex]);
            sttsData[sttsIndex] -= samplesThisTime;
            time += sampleDuration * samplesThisTime;
            samplesLeftInChunk -= samplesThisTime;
          }
          if (MP4_PARSER_DEBUG)
          {
            System.out.println("Done building MP4 time index map byte-time");
            for (int i = 0; i < chunkOffsets.length; i++)
            {
              System.out.println("Offset=" + chunkOffsets[i] + " \ttime=" + mp4TimeIndex[i]);;
            }
          }
          sttsIndex = -1;
          stscIndex = -1;
        }
      }
      else if (stscIndex >= 0)
      {
        bytesUntilNextAtom = 0;
        int[] stscData = (chunkQuality == 2) ? stscDataV : stscDataA;
        for (int x = 0; x + 7 < remAtomData.length; x+= 12)
        {
          int firstChunk = ((remAtomData[x] & 0xFF) << 24) | ((remAtomData[x+1] & 0xFF) << 16) | ((remAtomData[x+2] & 0xFF) << 8) | (remAtomData[x+3] & 0xFF);
          int sampPerChunk = ((remAtomData[x+4] & 0xFF) << 24) | ((remAtomData[x+5] & 0xFF) << 16) | ((remAtomData[x+6] & 0xFF) << 8) | (remAtomData[x+7] & 0xFF);
          bytesUntilNextAtom += 12;
          stscData[stscIndex++] = firstChunk;
          stscData[stscIndex++] = sampPerChunk;
          if (stscIndex == stscData.length)
          {
            stscIndex = -1;
            if (MP4_PARSER_DEBUG)
            {
              System.out.println("Done reading sample-to-chunk atom");
              for (int j = 0; j*2 < stscData.length; j++)
              {
                System.out.println("stsc chunk#" + stscData[2*j] + " \tsamplesPerChunk=" + stscData[2*j +1]);;
              }
            }
            break;
          }
        }
      }
      else if (sttsIndex >= 0)
      {
        bytesUntilNextAtom = 0;
        int[] sttsData = (chunkQuality == 2) ? sttsDataV : sttsDataA;
        for (int x = 0; x + 7 < remAtomData.length; x+= 8)
        {
          int sampleCount = ((remAtomData[x] & 0xFF) << 24) | ((remAtomData[x+1] & 0xFF) << 16) | ((remAtomData[x+2] & 0xFF) << 8) | (remAtomData[x+3] & 0xFF);
          int sampleDuration = ((remAtomData[x+4] & 0xFF) << 24) | ((remAtomData[x+5] & 0xFF) << 16) | ((remAtomData[x+6] & 0xFF) << 8) | (remAtomData[x+7] & 0xFF);
          bytesUntilNextAtom += 8;
          sttsData[sttsIndex++] = sampleCount;
          sttsData[sttsIndex++] = sampleDuration;
          if (sttsIndex == sttsData.length)
          {
            sttsIndex = -1;
            if (MP4_PARSER_DEBUG)
            {
              System.out.println("Done reading time-to-sample atom");
              for (int j = 0; j*2 < sttsData.length; j++)
              {
                System.out.println("stts sampleCount=" + sttsData[2*j] + " \tsampleDuration=" + sttsData[2*j +1]);;
              }
            }
            break;
          }
        }
      }
      else
      {
        currSize = ((remAtomData[0] & 0xFF) << 24) | ((remAtomData[1] & 0xFF) << 16) | ((remAtomData[2] & 0xFF) << 8) | (remAtomData[3] & 0xFF);
        if (currSize == 0)
          bytesUntilNextAtom = Long.MAX_VALUE; // goes to end of the file
        else if (currSize == 1)
        {
          // extended 64-bit size
          currSize = (((long)(remAtomData[8] & 0xFF)) << 56) | (((long)(remAtomData[9] & 0xFF)) << 48) | (((long)(remAtomData[10] & 0xFF)) << 40) |
              (((long)(remAtomData[11] & 0xFF)) << 32) | (((long)(remAtomData[12] & 0xFF)) << 24) | (((long)(remAtomData[13] & 0xFF)) << 16) |
              (((long)(remAtomData[14] & 0xFF)) << 8) | ((long)(remAtomData[15] & 0xFF));
          bytesUntilNextAtom = currSize;
        }
        else
          bytesUntilNextAtom = currSize;
        String type = ((char) remAtomData[4]) + "" + ((char) remAtomData[5]) + "" + ((char) remAtomData[6]) + "" + ((char) remAtomData[7]);
        if (MP4_PARSER_DEBUG) System.out.println("MP4 size="  + currSize + " type=" + type);
        if ("mdhd".equals(type))
        {
          timeScale = ((remAtomData[20] & 0xFF) << 24) | ((remAtomData[21] & 0xFF) << 16) | ((remAtomData[22] & 0xFF) << 8) | (remAtomData[23] & 0xFF);
          int duration = ((remAtomData[24] & 0xFF) << 24) | ((remAtomData[25] & 0xFF) << 16) | ((remAtomData[26] & 0xFF) << 8) | (remAtomData[27] & 0xFF);
          if (MP4_PARSER_DEBUG) System.out.println("timeScale=" + timeScale + " duration=" + duration + " REAL DURATION(seconds)=" + (duration/timeScale));
        }
        else if ("hdlr".equals(type))
        {
          // Check if it's audio or video
          String compType = ((char) remAtomData[12]) + "" + ((char) remAtomData[13]) + "" + ((char) remAtomData[14]) + "" + ((char) remAtomData[15]);
          String compSubtype = ((char) remAtomData[16]) + "" + ((char) remAtomData[17]) + "" + ((char) remAtomData[18]) + "" + ((char) remAtomData[19]);
          if (MP4_PARSER_DEBUG) System.out.println("MP4 Media compType=" + compType + " subtype=" + compSubtype);
          if (chunkQuality == -1)
          {
            if ("vide".equals(compSubtype))
              chunkQuality = 2;
            else if ("soun".equals(compSubtype))
              chunkQuality = 1;
          }
        }
        else if ("stsc".equals(type) && chunkQuality > 0)
        {
          int[] stscData = new int[2 * (((remAtomData[12] & 0xFF) << 24) | ((remAtomData[13] & 0xFF) << 16) |
              ((remAtomData[14] & 0xFF) << 8) | (remAtomData[15] & 0xFF))];
          if (chunkQuality == 2)
            stscDataV = stscData;
          else
            stscDataA = stscData;
          stscIndex = 0;
          bytesUntilNextAtom = 16;
          if (MP4_PARSER_DEBUG) System.out.println("Found sample-to-chunk atom");
        }
        else if ("stts".equals(type) && chunkQuality > 0)
        {
          int[] sttsData = new int[2 * (((remAtomData[12] & 0xFF) << 24) | ((remAtomData[13] & 0xFF) << 16) |
              ((remAtomData[14] & 0xFF) << 8) | (remAtomData[15] & 0xFF))];
          if (chunkQuality == 2)
            sttsDataV = sttsData;
          else
            sttsDataA = sttsData;
          sttsIndex = 0;
          bytesUntilNextAtom = 16;
          if (MP4_PARSER_DEBUG) System.out.println("Found time-to-sample atom");
        }
        else if ("stco".equals(type) && chunkQuality > 0)
        {
          // Parse this table so then we know what chunk # we are on for a given position in the stream (also handle 'co64')
          long[] chunkOffsets = new long[((remAtomData[12] & 0xFF) << 24) | ((remAtomData[13] & 0xFF) << 16) |
                                         ((remAtomData[14] & 0xFF) << 8) | (remAtomData[15] & 0xFF)];
          if (chunkQuality == 2)
            chunkOffsetsV = chunkOffsets;
          else
            chunkOffsetsA = chunkOffsets;
          stcoIndex = 0;
          bytesUntilNextAtom = 16;
          areOffsets64Bits = false;
          if (MP4_PARSER_DEBUG) System.out.println("Found chunk offset atom");
        }
        else if ("co64".equals(type) && chunkQuality > 0)
        {
          // Parse this table so then we know what chunk # we are on for a given position in the stream (also handle 'co64')
          long[] chunkOffsets = new long[((remAtomData[12] & 0xFF) << 24) | ((remAtomData[13] & 0xFF) << 16) |
                                         ((remAtomData[14] & 0xFF) << 8) | (remAtomData[15] & 0xFF)];
          if (chunkQuality == 2)
            chunkOffsetsV = chunkOffsets;
          else
            chunkOffsetsA = chunkOffsets;
          stcoIndex = 0;
          bytesUntilNextAtom = 16;
          areOffsets64Bits = true;
          if (MP4_PARSER_DEBUG) System.out.println("Found chunk offset (64-bit) atom");
        }
        else if ("mdat".equals(type))
        {
          if (chunkOffsetsV == null || mp4TimeIndexV == null)
          {
            if (Sage.DBG) System.out.println("MP4 file being downloaded is NOT STREAMBLE; the mdat section came before the sample tables");
            if (circSize > 0)
            {
              lastDownloadTimestamp = 5000; // to make it think we've got somewhere in the download
              indexAtEndOfMP4 = true;
              fullDownloadRequired = false;
            }
            else
              indexAtEndOfMP4 = fullDownloadRequired = true;
          }
          movieDataBegan = true;
        }
        if ("trak".equals(type))
          chunkQuality = -1; // signifies time to look for a new media type
        if ("moov".equals(type) || "trak".equals(type) || "mdia".equals(type) || "minf".equals(type) || "stbl".equals(type)) // it contains nested atoms
          bytesUntilNextAtom = 8;
      }
      if (bytesUntilNextAtom > 0)
      {
        if (remAtomSize > 0)
        {
          int used = Math.min((int)bytesUntilNextAtom, remAtomSize);
          if (used != remAtomSize)
          {
            System.arraycopy(remAtomData, used, remAtomData, 0, remAtomSize - used);
          }
          remAtomSize -= used;
          bytesUntilNextAtom -= used;
        }
        if (bytesUntilNextAtom > 0)
        {
          long used = Math.min(bytesUntilNextAtom, length);
          length -= used;
          offset += used;
          bytesUntilNextAtom -= used;
          if (length == 0)
            return;
        }
      }
    }
    if (length > 0)
    {
      System.arraycopy(buf, offset, remAtomData, remAtomSize, length);
      remAtomSize = length;
    }
  }

  private boolean processMP3Packet(byte[] buf, int offset, int length)
  {
    int index = 0;
    // scan through file to get syncword
    int headerstring = ((buf[index + offset] << 16) & 0x00FF0000)
        | ((buf[index+1+offset] << 8) & 0x0000FF00)
        | ((buf[index+2+offset] << 0) & 0x000000FF);
    boolean isSync = false;
    index += 2;
    do {
      headerstring <<= 8;
      index++;
      if (index+offset >= length) {
        // We didn't find the MP3 syncword so it probably isn't it
        return false;
      }

      headerstring |= (buf[index+offset] & 0x000000FF);
      isSync = ((headerstring & 0xFFE00000) == 0xFFE00000);
      // MPEG 2.5
      // filter out invalid sample rate
      if (isSync)
        isSync = (((headerstring >>> 10) & 3) != 3);
      // filter out invalid layer
      if (isSync)
        isSync = (((headerstring >>> 17) & 3) != 0);
      // filter out invalid version
      if (isSync)
        isSync = (((headerstring >>> 19) & 3) != 1);
    } while (!isSync);
    // Got a header... lets have a look at it...
    int h_version = ((headerstring >>> 19) & 1);
    if (((headerstring >>> 20) & 1) == 0) // SZD: MPEG2.5 detection
      if (h_version == MP3Utils.MPEG2_LSF)
        h_version = MP3Utils.MPEG25_LSF;
      else {
        if (Sage.DBG) System.out.println("invalid MPEG header (version)");
        return false;
      }
    int h_sample_frequency;
    if ((h_sample_frequency = ((headerstring >>> 10) & 3)) == 3) {
      if (Sage.DBG) System.out.println("invalid MPEG header (sample freq)");
      return false;
    }
    int h_layer = 4 - (headerstring >>> 17) & 3;
    int h_bitrate_index = (headerstring >>> 12) & 0xF;
    mp3Bitrate = MP3Utils.bitrates[h_version][h_layer - 1][h_bitrate_index];
    if (Sage.DBG) System.out.println("Detected mp3 bitrate of:" + mp3Bitrate);
    return true;
  }

  // NOTE: Not all MP4 content can be played back streaming. They may not have certain atom information
  // at the front of the file that is necessary to decode before playback. We judge this based off
  // the sample table. If we haven't hit it yet then we don't know if it's streamable. But we can tell if
  // we see a very large atom which is going to take the majority of the file that we know it will not be
  // streamable.

  private void stvTaskRun()
  {
    downloadedBytes = 0;
    if (xferBuf == null)
      xferBuf = new byte[Sage.getInt("file_transfer_buffer_size", 65536)];
    java.text.NumberFormat prctForm = java.text.NumberFormat.getPercentInstance();
    if (abort)
    {
      // In case someone is viewing while we're downloading
      Sage.processInactiveFile(myDestFile.toString());
      return;
    }
    long totalFileSize = remoteSize;
    try
    {
      long currOffset = 0;
      while (totalFileSize > 0)
      {
        if (abort)
        {
          // In case someone is viewing while we're downloading
          Sage.processInactiveFile(myDestFile.toString());
          return;
        }
        int currRead = (int)Math.min(xferBuf.length, totalFileSize);
        outStream.write(("READ " + currOffset + " " + currRead + "\r\n").getBytes(Sage.BYTE_CHARSET));
        outStream.flush();
        inStream.readFully(xferBuf, 0, currRead);
        if (circSize > 0)
        {
          waitForCircWrite(currRead);
          if (fileOut.position() + currRead < circSize)
            fileOut.write(xferBuf, 0, currRead);
          else
          {
            int firstWrite = (int)(circSize - fileOut.position());
            fileOut.write(xferBuf, 0, firstWrite);
            fileOut.seek(0);
            fileOut.write(xferBuf, firstWrite, currRead - firstWrite);
          }
        }
        else
          fileOut.write(xferBuf, 0, currRead);
        downloadedBytes += currRead;
        statusMessage = prctForm.format(((double)downloadedBytes)/remoteSize);
        totalFileSize -= currRead;
        currOffset += currRead;
      }
      outStream.write("QUIT\r\n".getBytes(Sage.BYTE_CHARSET));
      outStream.flush();
    }
    catch (Exception e)
    {
      if (Sage.DBG) System.out.println("ERROR during file download of:" + e);
      Sage.printStackTrace(e);
      statusMessage = "Error:" + e.toString();
      return;
    }
    finally
    {
      try{
        if (sock != null)
          sock.close();
      }catch (Exception e1){}
      try{
        if (outStream != null)
          outStream.close();
      }catch (Exception e2){}
      try{
        if (inStream != null)
          inStream.close();
      }catch (Exception e3){}
      try{
        if (fileOut != null)
          fileOut.close();
      }catch (Exception e4){}
    }

    succeeded();
    // In case someone is viewing while we're downloading..not needed for synchronous transfers
    if (!synchronous)
      Sage.processInactiveFile(myDestFile.toString());
  }

  private void remoteUITaskRun()
  {
    downloadedBytes = 0;
    java.text.NumberFormat prctForm = java.text.NumberFormat.getPercentInstance();
    MiniClientSageRenderer.RemoteFSXfer fsXferOp = (MiniClientSageRenderer.RemoteFSXfer) fsXferOpWeak.get();
    while (!fsXferOp.isDone())
    {
      if (abort)
      {
        fsXferOp.abortNow();
        return;
      }
      try{Thread.sleep(250);}catch(Exception e){}
      downloadedBytes = fsXferOp.getBytesXferd();
      statusMessage = prctForm.format(((double)downloadedBytes)/remoteSize);
    }

    if (fsXferOp.error != 0)
    {
      statusMessage = "Error:" + fsXferOp.error;
      return;
    }
    succeeded();
  }

  public long getNumDownloadedBytes()
  {
    return downloadedBytes;
  }

  public long getLastDownloadTimestamp()
  {
    return lastDownloadTimestamp;
  }

  public long getMinSeekTimestamp()
  {
    if (circSize > 0)
    {
      if (isMP4Stream && httpRangeSupported & !isComplete())
        return 0; // We can seek anywhere in this case
      else
      {
        // Only let them seek backwards into what we still have in the circular buffer. But this is
        // tricky because we'll need to track timestamps for this. For now just guess based on avg. bitrate
        // which we can easily get. And put the seek point 10% of the way into the buffer.
        if (downloadedBytes < circSize)
          return 0;
        else
        {
          long minSeekPoint = Math.max(lastDownloadOffset, downloadedBytes - circSize) + circSize/10;
          if (minSeekPoint >= downloadedBytes)
            return lastDownloadTimestamp;
          else
          {
            return lastDownloadTimestamp * minSeekPoint / downloadedBytes;
          }
        }
      }
    }
    else
      return 0; // the whole thing's being downloaded
  }

  public long getMaxSeekTimestamp()
  {
    if (circSize > 0 && isMP4Stream && httpRangeSupported)
      return Long.MAX_VALUE/10; // avoid overflows when this is used in math outside of here
    else
      return lastDownloadTimestamp;
  }

  // Returns true if things interested in this downloaders timing progress need
  // to make calls to find out more. (i.e. this is always true for circular files; but its false once a normal download completes)
  public boolean needsTracking()
  {
    return circSize > 0 || !isComplete();
  }

  public boolean isProgressivePlay()
  {
    return !fullDownloadRequired;
  }

  public long getCircularDownloadSize()
  {
    return circSize;
  }

  // This call is used during progressive download playback w/ circular file storage.
  // It's called so the downloader knows where the reading is currently at so it doesn't get too
  // far ahead and wrap around and write over what hasn't been played yet
  public void notifyReadRequest(long offset, long length)
  {
    lastNotifyReadOffset = offset;
    lastNotifyReadLength = length;
    if (CIRC_DOWNLOAD_DEBUG) System.out.println("notifyReadRequest=" + offset + " downloadedRange=" + Math.max(lastDownloadOffset, downloadedBytes - circSize) + " - " +
        downloadedBytes);
    if (circSize > 0 && !isComplete())
    {
      // Check to see if this read request is for data that's before what's in the buffer
      if (offset < Math.max(lastDownloadOffset, downloadedBytes - circSize))
      {
        //System.out.println("WARNING : Attempting to read from offset before the buffer: offset=" + offset);
        waitingForRead = true;
        while (alive && !abort && offset < Math.max(lastDownloadOffset, downloadedBytes - circSize))
        {
          synchronized (notifyRead)
          {
            try
            {
              notifyRead.wait(30);
            }
            catch (InterruptedException e){ }
          }
        }
        //System.out.println("DONE waiting for backwards read request to be ready!!");
        waitingForRead = false;
      }
      // Check to see if this read request is for data that's after what's in the buffer
      long maxRead = (remoteSize > 0) ? Math.min(remoteSize, offset + length) : (offset + length);
      if (maxRead > downloadedBytes/* && isMP4Stream*/)
      {
        //System.out.println("WARNING: Trying to read data beyond what's been downloaded diff=" + ((maxRead) - downloadedBytes));
        waitingForRead = true;
        while (alive && !abort && maxRead > downloadedBytes)
        {
          synchronized (notifyRead)
          {
            try
            {
              notifyRead.wait(30);
            }
            catch (InterruptedException e){ }
          }
        }
        //System.out.println("DONE waiting for forwards read request to be ready!!");
        waitingForRead = false;
      }
      synchronized (notifyRead)
      {
        notifyRead.notify();
        // Check if we need to flush this to disk yet in case the read from the circular file would be on data that's still
        // in the write buffer for the RAF
        if (offset + length > downloadedBytes - 65536)
        {
          try
          {
            if (fileOut != null)
              fileOut.flush();
          }
          catch (Exception e){}
        }
      }
    }
  }

  // Pauses and waits until it is OK to write data of the specified size to the circular file.
  protected void waitForCircWrite(int writeSize)
  {
    if (circSize > 0 && (downloadedBytes + writeSize) >= circSize)
    {
      // We want the player to be in the middle of the circular buffer. So if they're at least 40% of the
      // way into the buffer for their last read, we do this write.
      long markerPoint = Math.max(0, downloadedBytes - circSize) + circSize * 4 / 10;
      long minClientReadPoint = getSafeMP4SeekPositionForReadPosition(lastNotifyReadOffset, 0);
      if (CIRC_DOWNLOAD_DEBUG && markerPoint > minClientReadPoint)
        System.out.println("WAITING for circ buffer write to be OK");
      while (alive && markerPoint > minClientReadPoint && !abort && !waitingForRead)
      {
        synchronized (notifyRead)
        {
          try
          {
            notifyRead.wait(50);
          }
          catch (InterruptedException e){ }
        }
      }
    }
  }

  public boolean isClientWaitingForRead()
  {
    return waitingForRead;
  }

  public void notifyForPlayerSeek(long seekTimestamp)
  {
    if (CIRC_DOWNLOAD_DEBUG) System.out.println("Downloader notified of seek to: " + seekTimestamp);
    lastSeekNotifyTimestamp = seekTimestamp;
  }


  public long getRemoteSize() { return remoteSize; }

  //	private static boolean addedBouncy = false;
  private int decryptBuffer(byte[] xferBuf, int numRead, byte[] decryptBuf, boolean finalStep) throws Exception
  {
    // Here's where we can go to native code instead for the operation for performance reasons
    if (decryptCipher == null)
    {
      decryptKey = new javax.crypto.spec.SecretKeySpec(cryptoInfo.key, cryptoInfo.algorithm.substring(0, cryptoInfo.algorithm.indexOf('/')));
      /*			if (!addedBouncy)
			{
				java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
				addedBouncy = true;
			}*/
      decryptCipher = javax.crypto.Cipher.getInstance(cryptoInfo.algorithm);
      if (cryptoInfo.iv != null)
      {
        javax.crypto.spec.IvParameterSpec ivSpec = new javax.crypto.spec.IvParameterSpec(cryptoInfo.iv);
        decryptCipher.init(javax.crypto.Cipher.DECRYPT_MODE, decryptKey, ivSpec);
      }
      else
        decryptCipher.init(javax.crypto.Cipher.DECRYPT_MODE, decryptKey);
    }
    if (finalStep)
    {
      return decryptCipher.doFinal(xferBuf, 0, numRead, decryptBuf);
    }
    else
      return decryptCipher.update(xferBuf, 0, numRead, decryptBuf);
  }

  public static class DecryptInfo
  {
    public DecryptInfo(String algorithm, byte[] key, byte[] iv)
    {
      this.algorithm = algorithm;
      this.key = key;
      this.iv = iv;
    }
    public String algorithm;
    public byte[] key;
    public byte[] iv;
  }

  protected String myServerName;
  protected java.io.File mySrcFile;
  protected java.io.File myDestFile;
  protected java.net.Socket sock = null;
  protected java.io.DataOutputStream outStream = null;
  protected java.io.DataInputStream inStream = null;
  protected sage.io.SageFileSource fileOut = null;
  protected long remoteSize;
  protected boolean isMP4Stream;

  protected java.net.URL myURL;
  protected java.net.URLConnection myURLConn;
  protected java.io.InputStream urlinStream = null;

  protected long lastDownloadTimestamp;

  protected boolean remoteUIXfer;

  protected boolean gotMSAccess;
  protected boolean gotSMBAccess;

  protected boolean httpRangeSupported;

  protected long downloadedBytes;
  protected int mp3Bitrate;
  protected boolean fullDownloadRequired;
  protected boolean indexAtEndOfMP4;
  protected long circSize;
  protected long lastNotifyReadOffset;
  protected long lastNotifyReadLength;
  protected long lastDownloadOffset; // if we restart a download at a new offset, save it here
  protected Object notifyRead = new Object();
  protected boolean waitingForRead = false;
  protected long lastAutoReseekTime;


  // NOTE: Must use weak references here so we don't prevent GC of the UI stuff from
  // being a value in the WeakHashMap (who's value then has a strong reference to us, and the WeakHashMap values are strong refs as well)
  protected java.lang.ref.WeakReference uiMgrWeak;
  protected java.lang.ref.WeakReference fsXferOpWeak;

  protected boolean synchronous;
  protected byte[] xferBuf;
  protected boolean captureMode;

  protected DecryptInfo cryptoInfo;
  protected javax.crypto.Cipher decryptCipher;
  protected javax.crypto.spec.SecretKeySpec decryptKey;
}

<MSG> Merge pull request #10 from CraziFuzzy/master

Added property 'custom_user_agent' to override default.
<DFF> @@ -186,6 +186,8 @@ public class FileDownloader extends SystemTask
             myURLConn.setRequestProperty("Range", "bytes=0-");
           if (Sage.getBoolean("use_sagetv_user_agent", false))
             myURLConn.setRequestProperty("User-Agent", "SageTV/" + sage.Version.MAJOR_VERSION + "." + sage.Version.MINOR_VERSION + "." + sage.Version.MICRO_VERSION);
+          if (!"".equals(Sage.get("custom_user_agent", "")))
+            myURLConn.setRequestProperty("User-Agent", Sage.get("custom_user_agent", ""));
           if (requestProps != null)
           {
             java.util.Iterator walker = requestProps.entrySet().iterator();

{'commit': 'diff --git a/java/sage/FileDownloader.java b/java/sage/FileDownloader.java\nindex 89530979..46f630c1 100755\n--- a/java/sage/FileDownloader.java\n+++ b/java/sage/FileDownloader.java\n@@ -186,6 +186,8 @@ public class FileDownloader extends SystemTask\n             myURLConn.setRequestProperty("Range", "bytes=0-");\n           if (Sage.getBoolean("use_sagetv_user_agent", false))\n             myURLConn.setRequestProperty("User-Agent", "SageTV/" + sage.Version.MAJOR_VERSION + "." + sage.Version.MINOR_VERSION + "." + sage.Version.MICRO_VERSION);\n+          if (!"".equals(Sage.get("custom_user_agent", "")))\n+            myURLConn.setRequestProperty("User-Agent", Sage.get("custom_user_agent", ""));\n           if (requestProps != null)\n           {\n             java.util.Iterator walker = requestProps.entrySet().iterator();\n', 'commit_subject': 'Merge pull request #10 from CraziFuzzy/master', 'repo_name': 'google/sagetv', 'committer': {'name': 'Jeffrey Kardatzke', 'email': '149e99e399dbef4501468cca17a49793b7d0d4c6@google.com', 'time_sec': '1439574609', 'tz_offset': '-420', 'date': {'seconds': '1439574609'}}, 'commit_tree': '54d4bbb1bec8d65a0d535696f3287601f3320293', 'commit_message': "Merge pull request #10 from CraziFuzzy/master\n\nAdded property 'custom_user_agent' to override default.", 'language_name': 'Shell', 'license': 'apache-2.0', 'hunks': '@@ -186,6 +186,8 @@ public class FileDownloader extends SystemTask\n             myURLConn.setRequestProperty("Range", "bytes=0-");\n           if (Sage.getBoolean("use_sagetv_user_agent", false))\n             myURLConn.setRequestProperty("User-Agent", "SageTV/" + sage.Version.MAJOR_VERSION + "." + sage.Version.MINOR_VERSION + "." + sage.Version.MICRO_VERSION);\n+          if (!"".equals(Sage.get("custom_user_agent", "")))\n+            myURLConn.setRequestProperty("User-Agent", Sage.get("custom_user_agent", ""));\n           if (requestProps != null)\n           {\n             java.util.Iterator walker = requestProps.entrySet().iterator();\n', 'addition_count': 2, 'deletion_count': 0, 'src_file': 'a/java/sage/FileDownloader.java', 'tgt_file': 'b/java/sage/FileDownloader.java', 'file_extension': '.java', 'before_file': '/*\n * Copyright 2015 The SageTV Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the "License");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *  http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS IS" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage sage;\n\nimport sage.io.BufferedSageFile;\nimport sage.io.LocalSageFile;\nimport java.io.IOException;\nimport java.net.URI;\n\npublic class FileDownloader extends SystemTask\n{\n  private static final int MP4_RESEEK_PREROLL_TIME = 1000;\n  private static final boolean CIRC_DOWNLOAD_DEBUG = false;\n  private static java.util.Map uiXferMap = new java.util.WeakHashMap();\n  private static java.util.Map fileMap = new java.util.HashMap();\n  // This stores the status results from file download operations....in a way this is a memory leak since it\'ll grow without bound;\n  // but that growth should be very minimal and not cause any overall memory issue (similar to MetaImage caching all URL images forever)\n  private static java.util.Map resultMap = new java.util.HashMap();\n  public static FileDownloader getFileDownloader(UIManager uiMgr)\n  {\n    synchronized (uiXferMap)\n    {\n      if (uiXferMap.containsKey(uiMgr))\n        return (FileDownloader) uiXferMap.get(uiMgr);\n      FileDownloader rv = new FileDownloader(uiMgr);\n      uiXferMap.put(uiMgr, rv);\n      return rv;\n    }\n  }\n\n  public FileDownloader(UIManager inUIMgr)\n  {\n    if (inUIMgr != null)\n      uiMgrWeak = new java.lang.ref.WeakReference(inUIMgr);\n  }\n\n  public static FileDownloader getFileDownloader(java.io.File testFile)\n  {\n    synchronized (fileMap)\n    {\n      FileDownloader fd = (FileDownloader) fileMap.get(testFile);\n      if (fd != null && !fd.isComplete())\n        return fd;\n    }\n    return null;\n  }\n\n  public static Object getBGDownloadResult(java.io.File testFile)\n  {\n    return resultMap.get(testFile);\n  }\n\n  public static boolean isDownloading(java.io.File testFile)\n  {\n    return isDownloading(null, testFile);\n  }\n  public static boolean isDownloading(UIManager uiMgr, java.io.File testFile)\n  {\n    if (uiMgr == null)\n    {\n      return getFileDownloader(testFile) != null;\n    }\n    synchronized (uiXferMap)\n    {\n      FileDownloader fd = getFileDownloader(uiMgr);\n      return (fd.myDestFile != null && fd.myDestFile.equals(testFile) && !fd.isComplete());\n    }\n  }\n\n  public void cancel()\n  {\n    abort = true;\n    if (taskThread != null)\n    {\n      if (Sage.DBG) System.out.println("Waiting for download thread to terminate...");\n      try\n      {\n        // Forcibly close the HTTP connection if it\'s there so we don\'t worry about timeout issues\n        if (urlinStream != null)\n          urlinStream.close();\n      }\n      catch (Exception e){}\n      try\n      {\n        taskThread.join(30000);\n      }\n      catch (InterruptedException e)\n      {}\n      if (Sage.DBG) System.out.println("Download thread has terminated.");\n    }\n  }\n\n  public synchronized Object downloadCircularFile(String serverName, String srcFile, java.io.File destFile)\n  {\n    return downloadFile(serverName, srcFile, destFile, true);\n  }\n  // serverName can be null which means use the preferredServer, srcFile must be a file, it\'s downloaded to destFile which must also be a file\n  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile)\n  {\n    return downloadFile(serverName, srcFile, destFile, false);\n  }\n  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular)\n  {\n    return downloadFile(serverName, srcFile, destFile, circular, null);\n  }\n  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular, java.util.Properties requestProps)\n  {\n    return downloadFile(serverName, srcFile, destFile, circular, requestProps, false);\n  }\n  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular, java.util.Properties requestProps, boolean inSynchronous)\n  {\n    return downloadFile(serverName, srcFile, destFile, circular, requestProps, inSynchronous, false);\n  }\n  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular, java.util.Properties requestProps, boolean inSynchronous,\n      boolean captureMode)\n  {\n    return downloadFile(serverName, srcFile, destFile, circular, requestProps, inSynchronous, captureMode, null);\n  }\n  public synchronized Object downloadFile(String serverName, String srcFile, java.io.File destFile, boolean circular, java.util.Properties requestProps, boolean inSynchronous,\n      boolean captureMode, DecryptInfo cryptoSpec)\n  {\n    this.captureMode = captureMode;\n    this.cryptoInfo = cryptoSpec;\n    synchronous = inSynchronous;\n    UIManager uiMgr = (uiMgrWeak != null) ? ((UIManager) uiMgrWeak.get()) : null;\n    success = false;\n    if (circular && !Sage.client)\n      circSize = Sage.getLong("downloader/circular_file_size", 8*1024*1024);\n    else\n      circSize = 0;\n    indexAtEndOfMP4 = fullDownloadRequired = httpRangeSupported= false;\n    gotSMBAccess = gotMSAccess = false;\n    downloadedBytes = lastDownloadOffset = 0;\n    lastPlayerRequestTimestamp = lastSeekNotifyTimestamp = 0;\n    lastNotifyReadOffset = 0;\n    lastDownloadTimestamp = 0;\n    isMP4Stream = false;\n    statusMessage = "";\n    if (Sage.DBG) System.out.println("Download requested for files server=" + serverName + " src=" + srcFile + " dest=" + destFile);\n    if (Sage.client && serverName == null)\n      serverName = Sage.preferredServer;\n    remoteUIXfer = uiMgr != null && uiMgr.getUIClientType() == UIClient.REMOTE_UI && uiMgr.hasRemoteFSSupport();\n    if (serverName != null && (serverName.startsWith("http:") || serverName.startsWith("https:") || serverName.startsWith("ftp:") || serverName.startsWith("file:")))\n      remoteUIXfer = false;\n    if (serverName == null && !remoteUIXfer)\n      return Boolean.FALSE;\n\n    myServerName = serverName;\n    if (myServerName != null && myServerName.startsWith("smb://"))\n    {\n      srcFile = FSManager.getInstance().requestLocalSMBAccess(myServerName);\n      if (srcFile == null)\n        return Boolean.FALSE;\n      gotSMBAccess = true;\n    }\n    if (myServerName!=null && myServerName.startsWith("file:"))\n      mySrcFile = new java.io.File(URI.create(myServerName));\n    else\n      mySrcFile = (srcFile == null) ? null : new java.io.File(srcFile);\n\n    myDestFile = destFile;\n    if (!remoteUIXfer)\n    {\n      java.io.File destParent = myDestFile.getParentFile();\n      if (destParent != null)\n        IOUtils.safemkdirs(destParent);\n    }\n\n    if (myServerName != null && (myServerName.startsWith("http:") || myServerName.startsWith("https:") || myServerName.startsWith("ftp:")))\n    {\n      java.net.HttpURLConnection.setFollowRedirects(true);\n      try\n            myURLConn.setRequestProperty("Range", "bytes=0-");\n          if (Sage.getBoolean("use_sagetv_user_agent", false))\n            myURLConn.setRequestProperty("User-Agent", "SageTV/" + sage.Version.MAJOR_VERSION + "." + sage.Version.MINOR_VERSION + "." + sage.Version.MICRO_VERSION);\n          if (requestProps != null)\n          {\n            java.util.Iterator walker = requestProps.entrySet().iterator();\n          if (httpRangeSupported)\n            myURLConn.setRequestProperty("Range", "bytes=0-");\n          if (Sage.getBoolean("use_sagetv_user_agent", false))\n            myURLConn.setRequestProperty("User-Agent", "SageTV/" + sage.Version.MAJOR_VERSION + "." + sage.Version.MINOR_VERSION + "." + sage.Version.MICRO_VERSION);\n          if (!"".equals(Sage.get("custom_user_agent", "")))\n            myURLConn.setRequestProperty("User-Agent", Sage.get("custom_user_agent", ""));\n          if (requestProps != null)\n          {\n            java.util.Iterator walker = requestProps.entrySet().iterator();\n            while (walker.hasNext())\n            {\n              java.util.Map.Entry currEnt = (java.util.Map.Entry) walker.next();\n              if (currEnt.getKey() != null && currEnt.getValue() != null)\n                myURLConn.setRequestProperty(currEnt.getKey().toString(), currEnt.getValue().toString());\n            }\n          }\n          try\n          {\n            myURLConn.setConnectTimeout(30000);\n            myURLConn.setReadTimeout(30000);\n          }catch (Throwable tr){} // in case its a bad JRE version w/ out these calls\n          try\n          {\n            urlinStream = myURLConn.getInputStream();\n          }\n          catch (Exception e1)\n          {\n            // Check to see if HTTP Range requests are supported from this server\n            if (myURLConn instanceof java.net.HttpURLConnection && httpRangeSupported)\n            {\n              java.net.HttpURLConnection httpConn = (java.net.HttpURLConnection) myURLConn;\n              // 416 is the proper response for denying range requests; but some servers just send\n              // a generic 400 response (like last.fm)\n              if (httpConn.getResponseCode() / 100 == 4)\n              {\n                if (Sage.DBG) System.out.println("HTTP Range requests are NOT supported by this server. errcode=" + httpConn.getResponseCode());\n                httpRangeSupported = false;\n                continue;\n              }\n            }\n            throw e1;\n          }\n          remoteSize = myURLConn.getContentLength();\n          if (Sage.DBG) System.out.println("Download remoteSize=" + remoteSize);\n          if (myURLConn instanceof java.net.HttpURLConnection)\n          {\n            java.net.HttpURLConnection httpConn = (java.net.HttpURLConnection) myURLConn;\n            if (httpConn.getResponseCode() / 100 == 3)\n            {\n              if (Sage.DBG) System.out.println("Internally processing HTTP redirect...");\n              urlinStream.close();\n              myURL = new java.net.URL(httpConn.getHeaderField("Location"));\n              continue;\n            }\n          }\n          break;\n        }\n        fileOut = new BufferedSageFile(new LocalSageFile(myDestFile, false));\n        if (captureMode)\n          fileOut.seek(fileOut.length());\n        else\n          fileOut.setLength(0);\n      }\n      catch (Exception e)\n      {\n        System.out.println("ERROR with file download of:" + e);\n        try{\n          if (urlinStream != null)\n            urlinStream.close();\n        }catch (Exception e3){}\n        try{\n          if (fileOut != null)\n            fileOut.close();\n        }catch (Exception e4){}\n        cleanup();\n        return Boolean.FALSE;\n      }\n    }\n    else if (remoteUIXfer)\n    {\n      if (captureMode)\n      {\n        // capture mode not supported for remote UI xfers\n        return Boolean.FALSE;\n      }\n      if (!mySrcFile.isFile())\n      {\n        cleanup();\n        return Boolean.FALSE;\n      }\n\n      MiniClientSageRenderer mcsr = (MiniClientSageRenderer) uiMgr.getRootPanel().getRenderEngine();\n      remoteSize = mySrcFile.length();\n      MiniClientSageRenderer.RemoteFSXfer fsXferOp = mcsr.fsDownloadFile(mySrcFile, destFile.toString());\n      if (fsXferOp.error != 0)\n      {\n        cleanup();\n        return Boolean.FALSE;\n      }\n      fsXferOpWeak = new java.lang.ref.WeakReference(fsXferOp);\n    }\n    else if (myServerName!=null && mySrcFile!=null && mySrcFile.isFile() && myServerName.startsWith("file:"))\n    {\n      if (Sage.DBG) System.out.println("FileDownloader: Local File Copy: From: " + mySrcFile + " to " + destFile);\n      // file url passed as the url to download\n      if (!mySrcFile.exists())\n      {\n        cleanup();\n        return Boolean.FALSE;\n      }\n    }\n    else\n    {\n      try\n      {\n        sock = new java.net.Socket();\n        sock.connect(new java.net.InetSocketAddress(Sage.preferredServer, 7818), 5000);\n        sock.setSoTimeout(30000);\n        //sock.setTcpNoDelay(true);\n        outStream = new java.io.DataOutputStream(new java.io.BufferedOutputStream(sock.getOutputStream()));\n        inStream = new java.io.DataInputStream(new java.io.BufferedInputStream(sock.getInputStream()));\n        outStream.write("OPENW ".getBytes(Sage.BYTE_CHARSET));\n        outStream.write(mySrcFile.toString().getBytes("UTF-16BE"));\n        outStream.write("\\r\\n".getBytes(Sage.BYTE_CHARSET));\n        outStream.flush();\n        String str = Sage.readLineBytes(inStream);\n        if ("NON_MEDIA".equals(str))\n        {\n          // We may need to get special access to read this file; so try that first\n          if (NetworkClient.getSN().requestMediaServerAccess(mySrcFile, true))\n          {\n            gotMSAccess = true;\n            outStream.write("OPENW ".getBytes(Sage.BYTE_CHARSET));\n            outStream.write(mySrcFile.toString().getBytes("UTF-16BE"));\n            outStream.write("\\r\\n".getBytes(Sage.BYTE_CHARSET));\n            outStream.flush();\n            str = Sage.readLineBytes(inStream);\n          }\n        }\n        if (!"OK".equals(str))\n          throw new java.io.IOException("Error opening remote file of:" + str);\n        // get the size\n        outStream.write("SIZE\\r\\n".getBytes(Sage.BYTE_CHARSET));\n        outStream.flush();\n        str = Sage.readLineBytes(inStream);\n        remoteSize = Long.parseLong(str.substring(0, str.indexOf(\' \')));\n        fileOut = new BufferedSageFile(new LocalSageFile(myDestFile, false));\n        if (captureMode)\n          fileOut.seek(fileOut.length());\n        else\n          fileOut.setLength(0);\n      }\n      catch (Exception e)\n      {\n        System.out.println("ERROR with file download of:" + e);\n        try{\n          if (sock != null)\n            sock.close();\n        }catch (Exception e1){}\n        try{\n          if (outStream != null)\n            outStream.close();\n        }catch (Exception e2){}\n        try{\n          if (inStream != null)\n            inStream.close();\n        }catch (Exception e3){}\n        try{\n          if (fileOut != null)\n            fileOut.close();\n        }catch (Exception e4){}\n        cleanup();\n        return Boolean.FALSE;\n      }\n    }\n    // Don\'t allow others to look at our files if this was done in appendMode because it won\'t give proper information\n    if (!captureMode)\n    {\n      synchronized (fileMap)\n      {\n        fileMap.put(myDestFile, this);\n      }\n    }\n    startTaskThread("FileDownload", synchronous);\n\n    return (synchronous && !success) ? Boolean.FALSE : Boolean.TRUE;\n  }\n\n  public void taskRun()\n  {\n    try\n    {\n      if (myServerName != null && (myServerName.startsWith("http:") || myServerName.startsWith("https:") || myServerName.startsWith("ftp:")))\n      {\n        httpTaskRun();\n      }\n      else if (remoteUIXfer)\n      {\n        remoteUITaskRun();\n      }\n      else if (myServerName!=null && myServerName.startsWith("file:"))\n      {\n        fileTaskRun();\n      }\n      else\n      {\n        stvTaskRun();\n      }\n      if (Sage.DBG) System.out.println("Download completed for files server=" + myServerName + " src=" + mySrcFile + " dest=" + myDestFile + " abort=" + abort);\n    }\n    finally\n    {\n      cleanup();\n      if (uiMgrWeak == null)\n      {\n        resultMap.put(myDestFile, wasSuccessful() ? (Object)Boolean.TRUE : (Object)("Error: " + getStatusMessage()));\n      }\n      synchronized (fileMap)\n      {\n        fileMap.remove(myDestFile);\n      }\n    }\n  }\n\n  private void fileTaskRun() {\n    try {\n      downloadedBytes=myDestFile.length();\n      statusMessage="100%";\n\n      if (mySrcFile.equals(myDestFile)) {\n        succeeded();\n        return;\n      }\n\n      IOUtils.copyFile(mySrcFile, myDestFile);\n      succeeded();\n    }\n    catch (IOException e)\n    {\n      if (Sage.DBG) System.out.println("ERROR during file download/copy of:" + e);\n      Sage.printStackTrace(e);\n      statusMessage = "Error:" + e.toString();\n    }\n  }\n\n  private void cleanup()\n  {\n    if (gotMSAccess)\n    {\n      NetworkClient.getSN().requestMediaServerAccess(mySrcFile, false);\n      gotMSAccess = false;\n    }\n    if (gotSMBAccess)\n    {\n      FSManager.getInstance().releaseLocalSMBAccess(myServerName);\n      gotSMBAccess = false;\n    }\n\n  }\n\n  // NOTE: Not all servers support Range download requests!!!!\n  private void restartHttpDownload(long startOffset) throws java.io.IOException\n  {\n    if (startOffset > 0 && !httpRangeSupported)\n      throw new java.io.IOException("Cannot restart HTTP download because we already know this server doesn\'t support Range! offset=" + startOffset);\n    // Close current connection first\n    try{\n      if (urlinStream != null)\n        urlinStream.close();\n    }catch (Exception e3){}\n    myURLConn = myURL.openConnection();\n    if (httpRangeSupported)\n      myURLConn.setRequestProperty("Range", "bytes=" + startOffset + "-");\n    try\n    {\n      myURLConn.setConnectTimeout(30000);\n      myURLConn.setReadTimeout(30000);\n    }catch (Throwable tr){} // in case its a bad JRE version w/ out these calls\n    urlinStream = myURLConn.getInputStream();\n    if (myURLConn instanceof java.net.HttpURLConnection)\n    {\n      java.net.HttpURLConnection httpConn = (java.net.HttpURLConnection) myURLConn;\n      if (httpConn.getResponseCode() / 100 == 3)\n      {\n        if (Sage.DBG) System.out.println("Internally processing HTTP redirect to: " + httpConn.getHeaderField("Location") +\n            " responseCode=" + httpConn.getResponseCode());\n        urlinStream.close();\n        myURL = new java.net.URL(httpConn.getHeaderField("Location"));\n        if (abort) return;\n        restartHttpDownload(startOffset);\n        return;\n      }\n    }\n    lastDownloadOffset = downloadedBytes = startOffset;\n    //\t\tlastSeekNotifyTimestamp = 0; // reset this indicator\n    if (circSize > 0)\n    {\n      synchronized (notifyRead)\n      {\n        // Reposition this file\n        if (fileOut.length() < (startOffset % circSize))\n          fileOut.setLength(startOffset % circSize);\n        fileOut.seek(startOffset % circSize);\n        notifyRead.notify();\n      }\n    }\n    if (Sage.DBG) System.out.println("HTTP Download has been restarted at offset=" + startOffset);\n  }\n\n  // This\'ll reconnect to the server if it needs to\n  private int safeHttpRead(byte[] buffer) throws java.io.IOException\n  {\n    try\n    {\n      int rv = urlinStream.read(buffer);\n      // For encrypted files, due to partial decryption we can\'t trust the download size check here\n      if (rv <= 0 && remoteSize > 0 && downloadedBytes < remoteSize && httpRangeSupported && cryptoInfo == null)\n      {\n        if (Sage.DBG) System.out.println("Read " + rv + " bytes from the URL stream; but we\'re not done with the file yet...restart the download...");\n        restartHttpDownload(downloadedBytes);\n        return urlinStream.read(buffer);\n      }\n      return rv;\n    }\n    catch (java.io.IOException e)\n    {\n      if (httpRangeSupported && !abort)\n      {\n        if (Sage.DBG) System.out.println("HTTP read failed; trying to reconnect...");\n        restartHttpDownload(downloadedBytes);\n        return urlinStream.read(buffer);\n      }\n      else\n        throw e;\n    }\n  }\n\n  // NOTE: We should monitor the download and resume it if it hangs for some reason....that will definitely occur\n  // on large file transfers at some point or another\n  private void httpTaskRun()\n  {\n    downloadedBytes = 0;\n    if (xferBuf == null)\n      xferBuf = new byte[Sage.getInt("file_transfer_buffer_size", 65536)];\n    java.text.NumberFormat prctForm = java.text.NumberFormat.getPercentInstance();\n    if (abort)\n    {\n      // In case someone is viewing while we\'re downloading\n      Sage.processInactiveFile(myDestFile.toString());\n      return;\n    }\n    boolean isFlashStream = false;\n    boolean foundID3Tag = false;\n    int id3HeaderSize = 0;\n    boolean isMP3Stream = false;\n    boolean isSWFStream = false;\n    boolean isMpeg2PSStream = false;\n    boolean isMpeg2TSStream = false;\n    bytesUntilNextSizeTimestamp = 4;\n    numParsedSizeTimestampBytes = 0;\n    byte[] decryptBuf = null;\n    try\n    {\n      download_loop:\n        do\n        {\n          if (abort)\n          {\n            // In case someone is viewing while we\'re downloading\n            Sage.processInactiveFile(myDestFile.toString());\n            return;\n          }\n          if (circSize > 0)\n          {\n            if (lastNotifyReadOffset < Math.max(lastDownloadOffset, downloadedBytes - circSize))\n            {\n              if (Sage.DBG) System.out.println("DOWNLOADER restarting download to move the stream BACKWARDS notifyReadOffset=" + lastNotifyReadOffset +\n                  " lastSeekTimestamp=" + lastSeekNotifyTimestamp);\n              // Try to figure out where we should really seek too if we can\n              long reseekTarget = getSafeMP4SeekPositionForReadPosition(lastNotifyReadOffset, MP4_RESEEK_PREROLL_TIME);\n              /*\t\t\t\t\t\tif (isMP4Stream && lastSeekNotifyTimestamp > 0)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlong[] dataRange = getMinMaxByteRangeForTimestamp(lastSeekNotifyTimestamp);\n\t\t\t\t\t\t\tif (dataRange[0] < reseekTarget)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tif (Sage.DBG) System.out.println("Adjusting the backwards request to get all streams in it....was=" +\n\t\t\t\t\t\t\t\t\treseekTarget + " now=" + dataRange[0]);\n\t\t\t\t\t\t\t\treseekTarget = dataRange[0];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}*/\n              if (reseekTarget < Math.max(lastDownloadOffset, downloadedBytes - circSize))\n              {\n                bytesUntilNextAtom += downloadedBytes - reseekTarget; // if we\'re doing the initial MP4 parsing this is needed\n                restartHttpDownload(reseekTarget);\n                lastAutoReseekTime = Sage.time();\n              }\n              else if (Sage.DBG) System.out.println("Skipping re-seek since it\'s target is within our download buffer " + reseekTarget);\n            }\n            // Check to see if this read request is for data that\'s after what\'s in the buffer\n            else if (isMP4Stream && lastNotifyReadOffset > downloadedBytes + 256*1024)\n              /*\t\t\t\t\telse if (isMP4Stream && (lastNotifyReadOffset > downloadedBytes + circSize ||\n\t\t\t\t\t\t(lastSeekNotifyTimestamp != 0 && lastNotifyReadOffset > downloadedBytes + 128*1024)))*/\n              //\t\t\t\t\t\t((Sage.time() - lastAutoReseekTime) < 5000 ? (circSize*8/10) : 512*1024))\n            {\n              // Only skip MP4 streams ahead if it\'s more than the circular buffer (which is definitely a skip)\n              // or if the timestamp seek hint has been set as well...otherwise this is probably just another read\n              if (Sage.DBG) System.out.println("DOWNLOADER restarting download to move the stream FORWARDS notifyOffset=" + lastNotifyReadOffset +\n                  " lastSeekTimestamp=" + lastSeekNotifyTimestamp);\n              long reseekTarget = getSafeMP4SeekPositionForReadPosition(lastNotifyReadOffset, MP4_RESEEK_PREROLL_TIME);\n              /*\t\t\t\t\t\tif (lastSeekNotifyTimestamp > 0)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlong[] dataRange = getMinMaxByteRangeForTimestamp(lastSeekNotifyTimestamp);\n\t\t\t\t\t\t\tif (dataRange[0] < reseekTarget)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tif (Sage.DBG) System.out.println("Adjusting the forwards request to get all streams in it....was=" +\n\t\t\t\t\t\t\t\t\treseekTarget + " now=" + dataRange[0]);\n\t\t\t\t\t\t\t\treseekTarget = dataRange[0];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}*/\n              if (reseekTarget > downloadedBytes)\n              {\n                bytesUntilNextAtom -= reseekTarget - downloadedBytes; // if we\'re doing the initial MP4 parsing this is needed\n                restartHttpDownload(reseekTarget);\n                lastAutoReseekTime = Sage.time();\n              }\n              else if (Sage.DBG) System.out.println("Skipping re-seek since it\'s target is within our download buffer " + reseekTarget);\n            }\n          }\n          if (decryptBuf != null)\n          {\n            byte[] tempBuf = xferBuf;\n            // Swap the buffers back\n            xferBuf = decryptBuf;\n            decryptBuf = tempBuf;\n          }\n          int numRead = safeHttpRead(xferBuf);\n          if (numRead <= 0)\n          {\n            if (circSize > 0 && isMP4Stream && httpRangeSupported && indexAtEndOfMP4 && !abort)\n            {\n              if (Sage.DBG) System.out.println("Waiting at the end of the MP4 stream since we\'re probably just reading the header...");\n              fileOut.flush();\n            }\n            while (circSize > 0 && isMP4Stream && httpRangeSupported && indexAtEndOfMP4 && !abort)\n            {\n              if (lastNotifyReadOffset < Math.max(lastDownloadOffset, downloadedBytes - circSize))\n              {\n                continue download_loop;\n              }\n              synchronized (notifyRead)\n              {\n                try\n                {\n                  notifyRead.wait(30);\n                }\n                catch (InterruptedException e2){}\n              }\n            }\n            if (Sage.DBG) System.out.println("Read " + numRead + " bytes from the URL stream; download has completed");\n            if (cryptoInfo != null)\n            {\n              numRead = decryptBuffer(xferBuf, 0, decryptBuf, true);\n              if (numRead > 0)\n              {\n                if (circSize > 0)\n                {\n                  waitForCircWrite(numRead);\n                  synchronized (notifyRead)\n                  {\n                    if (fileOut.position() + numRead < circSize)\n                      fileOut.write(decryptBuf, 0, numRead);\n                    else\n                    {\n                      int firstWrite = (int)(circSize - fileOut.position());\n                      fileOut.write(decryptBuf, 0, firstWrite);\n                      fileOut.seek(0);\n                      fileOut.write(decryptBuf, firstWrite, numRead - firstWrite);\n                    }\n                  }\n                }\n                else\n                  fileOut.write(decryptBuf, 0, numRead);\n              }\n            }\n            break;\n          }\n          if (cryptoInfo != null)\n          {\n            if (decryptBuf == null)\n              decryptBuf = new byte[xferBuf.length*3/2]; // make it 50% bigger to handle the final buffer which may be larger\n            numRead = decryptBuffer(xferBuf, numRead, decryptBuf, false);\n            byte[] tempBuf = xferBuf;\n            // Swap the buffers now, then do it again before we download more data\n            xferBuf = decryptBuf;\n            decryptBuf = tempBuf;\n          }\n          if (circSize > 0)\n          {\n            waitForCircWrite(numRead);\n            synchronized (notifyRead)\n            {\n              if (fileOut.position() + numRead < circSize)\n                fileOut.write(xferBuf, 0, numRead);\n              else\n              {\n                int firstWrite = (int)(circSize - fileOut.position());\n                fileOut.write(xferBuf, 0, firstWrite);\n                fileOut.seek(0);\n                fileOut.write(xferBuf, firstWrite, numRead - firstWrite);\n              }\n            }\n          }\n          else\n            fileOut.write(xferBuf, 0, numRead);\n          if (downloadedBytes == 0 && numRead >= 10 && !isFlashStream && !isMP3Stream && !isMP4Stream && !isSWFStream && !isMpeg2PSStream && !isMpeg2TSStream)\n          {\n            // Check if it\'s a flash stream\n            if (xferBuf[0] == \'F\' && xferBuf[1] == \'L\' && xferBuf[2] == \'V\' && (((xferBuf[4] & 0x1) == 1) || ((xferBuf[4] & 0x4) == 4)))\n            {\n              if (Sage.DBG) System.out.println("Detected FLV stream downloading...");\n              isFlashStream = true;\n              processFlashPacket(xferBuf, 9, numRead - 9);\n            }\n            else if (xferBuf[0] == \'I\' && xferBuf[1] == \'D\' && xferBuf[2] == \'3\')\n            {\n              if (Sage.DBG) System.out.println("Detected ID3 tag in downloading...");\n              foundID3Tag = true;\n              id3HeaderSize = ((xferBuf[6]&0x7F)<<21) | ((xferBuf[7]&0x7F)<<14) |\n                  ((xferBuf[8]&0x7F)<<7) | (xferBuf[9]&0x7F);\n              id3HeaderSize += 10; // for the 10 initial bytes\n              isMP3Stream = true; // assume ID3 is MP3 for now\n            }\n            else if (xferBuf[4] == \'f\' && xferBuf[5] == \'t\' && xferBuf[6] == \'y\' && xferBuf[7] == \'p\')\n            {\n              // NOTE: This check could be much more solid....but this should do I\'m pretty sure. :)\n              if (Sage.DBG) System.out.println("Detected MP4 ISO stream downloading...");\n              isMP4Stream = true;\n              initMP4Parsing();\n              processMP4Packet(xferBuf, 0, numRead);\n            }\n            else if (xferBuf[4] == \'m\' && xferBuf[5] == \'d\' && xferBuf[6] == \'a\' && xferBuf[7] == \'t\')\n            {\n              // Handle quicktime files like MP4s for now....\n              if (Sage.DBG) System.out.println("Detected Quicktime stream downloading...");\n              isMP4Stream = true;\n              initMP4Parsing();\n              processMP4Packet(xferBuf, 0, numRead);\n            }\n            else if ((xferBuf[0] == \'C\' || xferBuf[0] == \'F\') && xferBuf[1] == \'W\' && xferBuf[2] == \'S\')\n            {\n              if (Sage.DBG) System.out.println("Detected SWF stream downloading...");\n              isSWFStream = true;\n            }\n            else if (xferBuf[0] == 0 && xferBuf[1] == 0 && xferBuf[2] == 1 && (xferBuf[3] & 0xFF) == 0xBA)\n            {\n              if (Sage.DBG) System.out.println("Detected MPEG2-PS stream downloading...");\n              isMpeg2PSStream = true;\n            }\n            else if (xferBuf[0] == 0x47 && (numRead < 189 || xferBuf[188] == 0x47) && (numRead < 377 || xferBuf[376] == 0x47))\n            {\n              if (Sage.DBG) System.out.println("Detected MPEG2-TS stream downloading...");\n              isMpeg2TSStream = true;\n            }\n            else if (processMP3Packet(xferBuf, 0, numRead))\n            {\n              if (Sage.DBG) System.out.println("Detected MPEG audio stream downloading...");\n              isMP3Stream = true;\n            }\n          }\n          else if (isFlashStream)\n            processFlashPacket(xferBuf, 0, numRead);\n          else if (isMP4Stream)\n            processMP4Packet(xferBuf, 0, numRead);\n          else if (isMP3Stream && mp3Bitrate == 0)\n          {\n            processMP3Packet(xferBuf, 0, numRead);\n          }\n          else if (isMpeg2PSStream && !captureMode)\n            processMpeg2PSPacket(xferBuf, 0, numRead);\n          else if (isMpeg2TSStream && !captureMode)\n            processMpeg2TSPacket(xferBuf, 0, numRead);\n          if (downloadedBytes == 0)\n          {\n            // So we don\'t have a zero length file on disk which\'ll fail verification\n            fileOut.flush();\n          }\n          downloadedBytes += numRead;\n          if (CIRC_DOWNLOAD_DEBUG) System.out.println("DownloadedBytes=" + downloadedBytes);\n          if (isMP3Stream && downloadedBytes > id3HeaderSize && mp3Bitrate > 0)\n          {\n            // Estimate the MP3 time that we\'ve downloaded (milliseconds)\n            lastDownloadTimestamp = (downloadedBytes - id3HeaderSize)*8000 / mp3Bitrate;\n          }\n          if (remoteSize > 0)\n            statusMessage = prctForm.format(((double)downloadedBytes)/remoteSize);\n          else\n            statusMessage = (downloadedBytes/1024) + " KB";\n        }while (true);\n    }\n    catch (Exception e)\n    {\n      if (Sage.DBG) System.out.println("ERROR during file download of:" + e);\n      Sage.printStackTrace(e);\n      statusMessage = "Error:" + e.toString();\n      return;\n    }\n    finally\n    {\n      try{\n        if (urlinStream != null)\n          urlinStream.close();\n      }catch (Exception e3){}\n      try{\n        if (fileOut != null)\n          fileOut.close();\n      }catch (Exception e4){}\n      cleanupMP4Parsing();\n    }\n\n    succeeded();\n    // In case someone is viewing while we\'re downloading\n    if (!captureMode)\n      Sage.processInactiveFile(myDestFile.toString());\n  }\n\n\n  // We have the type+size+timestamp(1+3+3)\n  private int bytesUntilNextSizeTimestamp = 4; // 4 byte offset into the first packet\n  private int numParsedSizeTimestampBytes = 0;\n  private byte[] sizeTimestampData = new byte[7];\n  private void processFlashPacket(byte[] buf, int offset, int length)\n  {\n    while (length > 0)\n    {\n      if (bytesUntilNextSizeTimestamp > 0)\n      {\n        if (bytesUntilNextSizeTimestamp < length)\n        {\n          offset += bytesUntilNextSizeTimestamp;\n          length -= bytesUntilNextSizeTimestamp;\n          bytesUntilNextSizeTimestamp = 0;\n          continue;\n        }\n        else\n        {\n          bytesUntilNextSizeTimestamp -= length;\n          return;\n        }\n      }\n      else\n      {\n        int neededBytes = 7 - numParsedSizeTimestampBytes;\n        int availBytes = Math.min(neededBytes, length);\n        System.arraycopy(buf, offset, sizeTimestampData, numParsedSizeTimestampBytes, availBytes);\n        numParsedSizeTimestampBytes += availBytes;\n        offset += availBytes;\n        length -= availBytes;\n\n        if (numParsedSizeTimestampBytes == 7)\n        {\n          // We have a complete type+size + timestamp\n          int streamType = sizeTimestampData[0];\n          int tagBodyLength = ((sizeTimestampData[1] & 0xFF) << 16) + ((sizeTimestampData[2] & 0xFF) << 8) + (sizeTimestampData[3] & 0xFF);\n          int tagTimestamp = ((sizeTimestampData[4] & 0xFF) << 16) + ((sizeTimestampData[5] & 0xFF) << 8) + (sizeTimestampData[6] & 0xFF);\n          if (streamType == 9) // video\n          {\n            lastDownloadTimestamp = tagTimestamp;\n          }\n          numParsedSizeTimestampBytes = 0;\n          bytesUntilNextSizeTimestamp = tagBodyLength + 8;\n        }\n      }\n    }\n  }\n\n  private void processMpeg2PSPacket(byte[] buf, int offset, int length)\n  {\n    // We just search for the PTS\'s in the 0xe0 video stream and use those for the latest timestamp\n    for (int i = offset; i < offset + length - 27; i++)\n    {\n      if (buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1 && (buf[i + 3] & 0xFF) == 0xBA)\n      {\n        // Pack header is here\n        // Skip over the data in the pack header and land on the pack_stuffing_length byte (it uses the lower 3 bits)\n        i += 13;\n        // Check for any stuffing bytes in the pack header and skip them\n        i += (1 + (buf[i] & 0x7));\n        if (i + 13 >= offset + length) break;\n        // Check for a start code next, it can be a system header or a pes packet\n        while (buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1 && (buf[i + 3] & 0xFF) != 0xBA)\n        {\n          i += 3;\n          if ((buf[i] & 0xFF) == 0xBB)\n          {\n            // system header, skip it\n            i += 3 + (((buf[i + 1] & 0xFF) << 8) | (buf[i + 2] & 0xFF));\n            // Check for the start code next\n            if (i + 15 >= offset + length) break;\n            if (buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 1)\n              continue;\n            else\n              i += 3;\n          }\n          if (i + 15 >= offset + length) break;\n          int stream_id = buf[i] & 0xFF;\n          int packet_length = ((buf[i + 1] & 0xFF) << 8) | (buf[i + 2] & 0xFF);\n          //System.out.println("PES stream id=0x" + Integer.toString(stream_id, 16) + " length=" + packet_length);\n          if (stream_id == 0xBD || (stream_id >= 0xC0 && stream_id <= 0xEF))\n          {\n            //System.out.println("Audio/video substream found");\n            // Valid audio or video stream, check for the pts flag\n            if ((buf[i + 4] & 0x80) == 0x80)\n            {\n              // PTS found, extract it\n              long pts = (((long)(buf[i + 6] & 0x0E)) << 29) |\n                  ((buf[i + 7] & 0xFF) << 22) |\n                  ((buf[i + 8] & 0xFE) << 14) |\n                  ((buf[i + 9] & 0xFF) << 7) |\n                  ((buf[i + 10] & 0xFE) >> 1);\n              lastDownloadTimestamp = pts / 90; // convert to milliseconds\n            }\n          }\n        }\n      }\n    }\n  }\n\n  private void processMpeg2TSPacket(byte[] buf, int offset, int length)\n  {\n    for (int i = offset; i < offset + length - 188; i++)\n    {\n      // Align on a TS sync code\n      if (buf[i] == 0x47)\n      {\n        // Find a payload start indicator, that\'s the only way there\'s PTS at the head\n        if ((buf[i + 1] & 0x40) != 0)\n        {\n          int adaptation_field_control = (buf[i + 3] & 0x30) >> 4;\n    if (adaptation_field_control == 2)\n      i += (188 - 4);\n    else if (adaptation_field_control == 3)\n      i += Math.min(188 - 6, 1 + (buf[i + 4] & 0xFF));\n    i += 4;\n    if (adaptation_field_control == 1 || adaptation_field_control == 3)\n    {\n      // Check for a start code next, it can be a system header or a pes packet\n      if (buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1)\n      {\n        i += 3;\n        if (i + 15 >= offset + length) break;\n        int stream_id = buf[i] & 0xFF;\n        //System.out.println("PES stream id=0x" + Integer.toString(stream_id, 16));\n        if ((stream_id == 0xBD || (stream_id >= 0xC0 && stream_id <= 0xEF) || (stream_id == 0xFD)))\n        {\n          //System.out.println("Audio/video substream found");\n          // Valid audio or video stream, check for the pts flag\n          if ((buf[i + 4] & 0x80) == 0x80)\n          {\n            // PTS found, extract it\n            long pts = (((long)(buf[i + 6] & 0x0E)) << 29) |\n                ((buf[i + 7] & 0xFF) << 22) |\n                ((buf[i + 8] & 0xFE) << 14) |\n                ((buf[i + 9] & 0xFF) << 7) |\n                ((buf[i + 10] & 0xFE) >> 1);\n            lastDownloadTimestamp = pts / 90; // convert to milliseconds\n          }\n        }\n      }\n    }\n        }\n      }\n    }\n  }\n\n  private long bytesUntilNextAtom = 0;\n  private byte[] remAtomData = new byte[28];\n  private int remAtomSize = 0;\n  private int[] sttsDataA; // even are the counts, odd are the durations\n  private int[] sttsDataV; // even are the counts, odd are the durations\n  private int sttsIndex;\n  private int[] stscDataA; // even are the first chunk #, odd are the samples per chunk\n  private int[] stscDataV; // even are the first chunk #, odd are the samples per chunk\n  private int stscIndex;\n  private long[] chunkOffsetsA; // one for each chunk\n  private long[] chunkOffsetsV; // one for each chunk\n  private int stcoIndex;\n  private boolean areOffsets64Bits;\n  private int chunkQuality;\n  private int timeScale;\n  private long[] mp4TimeIndexA = null; // chunkOffsets has the positions, this is the same length and has the times\n  private long[] mp4TimeIndexV = null; // chunkOffsets has the positions, this is the same length and has the times\n  private boolean movieDataBegan;\n  private long lastSeekNotifyTimestamp;\n  private long lastPlayerRequestTimestamp;\n  private static final boolean MP4_PARSER_DEBUG = false;\n  private void initMP4Parsing()\n  {\n    bytesUntilNextAtom = 0;\n    remAtomSize = 0;\n    sttsIndex = stscIndex = stcoIndex = -1;\n    chunkQuality = 0;\n    movieDataBegan = false;\n  }\n  private void cleanupMP4Parsing()\n  {\n    chunkOffsetsA = chunkOffsetsV = null;\n    sttsDataA = sttsDataV = stscDataA = stscDataV = null;\n    mp4TimeIndexA = mp4TimeIndexV = null;\n  }\n  private long findTimestampForBytePos(long pos, long[] bytePos, long[] timestamps)\n  {\n    int idx = java.util.Arrays.binarySearch(bytePos, pos);\n    if (idx < 0)\n      idx = (-(idx + 1)) - 1; // find the insertion point then backup one\n    if (idx < 0)\n      idx = 0; // don\'t go before zero\n    return timestamps[idx];\n  }\n  private long findBytePosForTimestamp(long timestamp, long[] bytePos, long[] timestamps)\n  {\n    int idx = java.util.Arrays.binarySearch(timestamps, timestamp);\n    if (idx < 0)\n      idx = (-(idx + 1)) - 1; // find the insertion point then backup one\n    if (idx < 0)\n      idx = 0; // don\'t go before zero\n    return bytePos[idx];\n  }\n  private long[] getMinMaxByteRangeForTimestamp(long timestamp)\n  {\n    if (!isMP4Stream)\n      return null;\n    long min = Long.MAX_VALUE;\n    long max = 0;\n    if (chunkOffsetsA != null && mp4TimeIndexA != null)\n    {\n      long apos = findBytePosForTimestamp(timestamp, chunkOffsetsA, mp4TimeIndexA);\n      min = Math.min(apos, min);\n      max = Math.max(apos, max);\n    }\n    if (chunkOffsetsV != null && mp4TimeIndexV != null)\n    {\n      long vpos = findBytePosForTimestamp(timestamp, chunkOffsetsV, mp4TimeIndexV);\n      min = Math.min(vpos, min);\n      max = Math.max(vpos, max);\n    }\n    return new long[] { min, max };\n  }\n  private long getSafeMP4SeekPositionForReadPosition(long readPos, long timeOffset)\n  {\n    if (isMP4Stream && chunkOffsetsV != null && mp4TimeIndexV != null && mp4TimeIndexA != null &&\n        chunkOffsetsA != null)\n    {\n      // Check to see if we think we know exactly what time they\'re looking for:\n      int testidx = java.util.Arrays.binarySearch(chunkOffsetsA, readPos);\n      long atime, vtime;\n      if (testidx >= 0)\n      {\n        if (CIRC_DOWNLOAD_DEBUG) System.out.println("ESTIMATING-A seek time to be:" + mp4TimeIndexA[testidx]);\n        atime = vtime = mp4TimeIndexA[testidx];\n      }\n      else if ((testidx = java.util.Arrays.binarySearch(chunkOffsetsV, readPos)) >= 0)\n      {\n        if (CIRC_DOWNLOAD_DEBUG) System.out.println("ESTIMATING-V seek time to be:" + mp4TimeIndexV[testidx]);\n        atime = vtime = mp4TimeIndexV[testidx];\n      }\n      else\n      {\n        // Figure out what are the two potential timestamps for this read position.\n        // Then take the minimum of those two values.\n        // Then look up the positions for each stream for that timestamp.\n        // Then use the minimum of those two positions for where we think the client is reading from currently.\n        atime = findTimestampForBytePos(readPos, chunkOffsetsA, mp4TimeIndexA);\n        vtime = findTimestampForBytePos(readPos, chunkOffsetsV, mp4TimeIndexV);\n        long maxtime, mintime;\n        if (atime > vtime)\n        {\n          maxtime = atime;\n          mintime = vtime;\n        }\n        else\n        {\n          maxtime = vtime;\n          mintime = atime;\n        }\n        if (CIRC_DOWNLOAD_DEBUG) System.out.println("ESTIMATING A-V seek times to be A=" + atime + " V=" + vtime);\n      }\n      long testTime = Math.min(atime, vtime);\n      testTime = Math.max(0, testTime - timeOffset); // for keyframe alignment\n      atime = findBytePosForTimestamp(testTime, chunkOffsetsA, mp4TimeIndexA);\n      vtime = findBytePosForTimestamp(testTime, chunkOffsetsV, mp4TimeIndexV);\n      return Math.min(readPos, Math.min(atime, vtime));\n    }\n    else\n      return readPos;\n  }\n\n  private void processMP4Packet(byte[] buf, int offset, int length)\n  {\n    if (movieDataBegan)\n    {\n      long newMinTimestamp = Long.MAX_VALUE;\n      if (!fullDownloadRequired)\n      {\n        if (chunkOffsetsA != null && mp4TimeIndexA != null)\n        {\n          newMinTimestamp = Math.min(findTimestampForBytePos(downloadedBytes, chunkOffsetsA, mp4TimeIndexA), newMinTimestamp);\n        }\n        if (chunkOffsetsV != null && mp4TimeIndexV != null)\n        {\n          newMinTimestamp = Math.min(findTimestampForBytePos(downloadedBytes, chunkOffsetsV, mp4TimeIndexV), newMinTimestamp);\n        }\n        if (newMinTimestamp != Long.MAX_VALUE)\n          lastDownloadTimestamp = newMinTimestamp;\n      }\n      if (!MP4_PARSER_DEBUG && (!indexAtEndOfMP4 || (mp4TimeIndexA != null && mp4TimeIndexV != null)))\n        return;\n    }\n    if (bytesUntilNextAtom > 0)\n    {\n      if (remAtomSize > 0)\n      {\n        int used = Math.min((int)bytesUntilNextAtom, remAtomSize);\n        if (used != remAtomSize)\n        {\n          System.arraycopy(remAtomData, used, remAtomData, 0, remAtomSize - used);\n        }\n        remAtomSize -= used;\n        bytesUntilNextAtom -= used;\n      }\n      if (bytesUntilNextAtom > 0)\n      {\n        long used = Math.min(bytesUntilNextAtom, length);\n        length -= used;\n        offset += used;\n        bytesUntilNextAtom -= used;\n        if (length == 0)\n          return;\n      }\n    }\n\n    while ((length + remAtomSize >= remAtomData.length && bytesUntilNextAtom >= 0) || (downloadedBytes + offset + length == remoteSize && length > 0))\n    {\n      System.arraycopy(buf, offset, remAtomData, remAtomSize, Math.min(remAtomData.length - remAtomSize, length));\n      long currSize;\n      if (stcoIndex >= 0)\n      {\n        long currVal;\n        bytesUntilNextAtom = 0;\n        long[] chunkOffsets = (chunkQuality == 2) ? chunkOffsetsV : chunkOffsetsA;\n        if (areOffsets64Bits)\n        {\n          for (int x = 0; x + 7 < remAtomData.length; x+=8)\n          {\n            currVal = ((remAtomData[x] & 0xFF) << 56) | ((remAtomData[x+1] & 0xFF) << 48) | ((remAtomData[x+2] & 0xFF) << 40) | ((remAtomData[x+3] & 0xFF) << 32) |\n                ((remAtomData[x+4] & 0xFF) << 24) | ((remAtomData[x+5] & 0xFF) << 16) | ((remAtomData[x+6] & 0xFF) << 8) | (remAtomData[x+7] & 0xFF);\n            bytesUntilNextAtom += 8;\n            chunkOffsets[stcoIndex++] = currVal;\n            if (stcoIndex == chunkOffsets.length)\n            {\n              stcoIndex = -1;\n              if (MP4_PARSER_DEBUG) System.out.println("Done reading chunk offset atom");\n              break;\n            }\n          }\n        }\n        else\n        {\n          for (int x = 0; x + 3 < remAtomData.length; x+=4)\n          {\n            currVal = ((remAtomData[x] & 0xFF) << 24) | ((remAtomData[x+1] & 0xFF) << 16) | ((remAtomData[x+2] & 0xFF) << 8) | (remAtomData[x+3] & 0xFF);\n            bytesUntilNextAtom += 4;\n            chunkOffsets[stcoIndex++] = currVal;\n            if (stcoIndex == chunkOffsets.length)\n            {\n              stcoIndex = -1;\n              if (MP4_PARSER_DEBUG) System.out.println("Done reading chunk offset atom");\n              break;\n            }\n          }\n        }\n        if (stcoIndex == -1)\n        {\n          // We\'re done parsing the sample information so now we can build the time-byte index\n          if (Sage.DBG) System.out.println("Building MP4 byte-time index map to track downloading");\n          long[] mp4TimeIndex = new long[chunkOffsets.length];\n          if (chunkQuality == 2)\n            mp4TimeIndexV = mp4TimeIndex;\n          else\n            mp4TimeIndexA = mp4TimeIndex;\n          int chunkNum = 0;\n          long sampleNum = 0;\n          sttsIndex = 0;\n          stscIndex = 0;\n          long time = 0;\n          long sampleDuration;\n          int samplesLeftInChunk = 0;\n          int[] stscData = (chunkQuality == 2) ? stscDataV : stscDataA;\n          int[] sttsData = (chunkQuality == 2) ? sttsDataV : sttsDataA;\n          while (true)\n          {\n            if (samplesLeftInChunk == 0)\n            {\n              mp4TimeIndex[chunkNum] = time;\n              chunkNum++;\n              if (chunkNum == chunkOffsets.length)\n                break;\n              if (stscIndex + 2 < stscData.length && chunkNum >= stscData[stscIndex + 2])\n                stscIndex += 2;\n              samplesLeftInChunk = stscData[stscIndex + 1];\n            }\n            if (sttsData[sttsIndex] == 0)\n              sttsIndex+=2;\n            sampleDuration = sttsData[sttsIndex + 1] * 1000 / timeScale;\n\n            int samplesThisTime = Math.min(samplesLeftInChunk, sttsData[sttsIndex]);\n            sttsData[sttsIndex] -= samplesThisTime;\n            time += sampleDuration * samplesThisTime;\n            samplesLeftInChunk -= samplesThisTime;\n          }\n          if (MP4_PARSER_DEBUG)\n          {\n            System.out.println("Done building MP4 time index map byte-time");\n            for (int i = 0; i < chunkOffsets.length; i++)\n            {\n              System.out.println("Offset=" + chunkOffsets[i] + " \\ttime=" + mp4TimeIndex[i]);;\n            }\n          }\n          sttsIndex = -1;\n          stscIndex = -1;\n        }\n      }\n      else if (stscIndex >= 0)\n      {\n        bytesUntilNextAtom = 0;\n        int[] stscData = (chunkQuality == 2) ? stscDataV : stscDataA;\n        for (int x = 0; x + 7 < remAtomData.length; x+= 12)\n        {\n          int firstChunk = ((remAtomData[x] & 0xFF) << 24) | ((remAtomData[x+1] & 0xFF) << 16) | ((remAtomData[x+2] & 0xFF) << 8) | (remAtomData[x+3] & 0xFF);\n          int sampPerChunk = ((remAtomData[x+4] & 0xFF) << 24) | ((remAtomData[x+5] & 0xFF) << 16) | ((remAtomData[x+6] & 0xFF) << 8) | (remAtomData[x+7] & 0xFF);\n          bytesUntilNextAtom += 12;\n          stscData[stscIndex++] = firstChunk;\n          stscData[stscIndex++] = sampPerChunk;\n          if (stscIndex == stscData.length)\n          {\n            stscIndex = -1;\n            if (MP4_PARSER_DEBUG)\n            {\n              System.out.println("Done reading sample-to-chunk atom");\n              for (int j = 0; j*2 < stscData.length; j++)\n              {\n                System.out.println("stsc chunk#" + stscData[2*j] + " \\tsamplesPerChunk=" + stscData[2*j +1]);;\n              }\n            }\n            break;\n          }\n        }\n      }\n      else if (sttsIndex >= 0)\n      {\n        bytesUntilNextAtom = 0;\n        int[] sttsData = (chunkQuality == 2) ? sttsDataV : sttsDataA;\n        for (int x = 0; x + 7 < remAtomData.length; x+= 8)\n        {\n          int sampleCount = ((remAtomData[x] & 0xFF) << 24) | ((remAtomData[x+1] & 0xFF) << 16) | ((remAtomData[x+2] & 0xFF) << 8) | (remAtomData[x+3] & 0xFF);\n          int sampleDuration = ((remAtomData[x+4] & 0xFF) << 24) | ((remAtomData[x+5] & 0xFF) << 16) | ((remAtomData[x+6] & 0xFF) << 8) | (remAtomData[x+7] & 0xFF);\n          bytesUntilNextAtom += 8;\n          sttsData[sttsIndex++] = sampleCount;\n          sttsData[sttsIndex++] = sampleDuration;\n          if (sttsIndex == sttsData.length)\n          {\n            sttsIndex = -1;\n            if (MP4_PARSER_DEBUG)\n            {\n              System.out.println("Done reading time-to-sample atom");\n              for (int j = 0; j*2 < sttsData.length; j++)\n              {\n                System.out.println("stts sampleCount=" + sttsData[2*j] + " \\tsampleDuration=" + sttsData[2*j +1]);;\n              }\n            }\n            break;\n          }\n        }\n      }\n      else\n      {\n        currSize = ((remAtomData[0] & 0xFF) << 24) | ((remAtomData[1] & 0xFF) << 16) | ((remAtomData[2] & 0xFF) << 8) | (remAtomData[3] & 0xFF);\n        if (currSize == 0)\n          bytesUntilNextAtom = Long.MAX_VALUE; // goes to end of the file\n        else if (currSize == 1)\n        {\n          // extended 64-bit size\n          currSize = (((long)(remAtomData[8] & 0xFF)) << 56) | (((long)(remAtomData[9] & 0xFF)) << 48) | (((long)(remAtomData[10] & 0xFF)) << 40) |\n              (((long)(remAtomData[11] & 0xFF)) << 32) | (((long)(remAtomData[12] & 0xFF)) << 24) | (((long)(remAtomData[13] & 0xFF)) << 16) |\n              (((long)(remAtomData[14] & 0xFF)) << 8) | ((long)(remAtomData[15] & 0xFF));\n          bytesUntilNextAtom = currSize;\n        }\n        else\n          bytesUntilNextAtom = currSize;\n        String type = ((char) remAtomData[4]) + "" + ((char) remAtomData[5]) + "" + ((char) remAtomData[6]) + "" + ((char) remAtomData[7]);\n        if (MP4_PARSER_DEBUG) System.out.println("MP4 size="  + currSize + " type=" + type);\n        if ("mdhd".equals(type))\n        {\n          timeScale = ((remAtomData[20] & 0xFF) << 24) | ((remAtomData[21] & 0xFF) << 16) | ((remAtomData[22] & 0xFF) << 8) | (remAtomData[23] & 0xFF);\n          int duration = ((remAtomData[24] & 0xFF) << 24) | ((remAtomData[25] & 0xFF) << 16) | ((remAtomData[26] & 0xFF) << 8) | (remAtomData[27] & 0xFF);\n          if (MP4_PARSER_DEBUG) System.out.println("timeScale=" + timeScale + " duration=" + duration + " REAL DURATION(seconds)=" + (duration/timeScale));\n        }\n        else if ("hdlr".equals(type))\n        {\n          // Check if it\'s audio or video\n          String compType = ((char) remAtomData[12]) + "" + ((char) remAtomData[13]) + "" + ((char) remAtomData[14]) + "" + ((char) remAtomData[15]);\n          String compSubtype = ((char) remAtomData[16]) + "" + ((char) remAtomData[17]) + "" + ((char) remAtomData[18]) + "" + ((char) remAtomData[19]);\n          if (MP4_PARSER_DEBUG) System.out.println("MP4 Media compType=" + compType + " subtype=" + compSubtype);\n          if (chunkQuality == -1)\n          {\n            if ("vide".equals(compSubtype))\n              chunkQuality = 2;\n            else if ("soun".equals(compSubtype))\n              chunkQuality = 1;\n          }\n        }\n        else if ("stsc".equals(type) && chunkQuality > 0)\n        {\n          int[] stscData = new int[2 * (((remAtomData[12] & 0xFF) << 24) | ((remAtomData[13] & 0xFF) << 16) |\n              ((remAtomData[14] & 0xFF) << 8) | (remAtomData[15] & 0xFF))];\n          if (chunkQuality == 2)\n            stscDataV = stscData;\n          else\n            stscDataA = stscData;\n          stscIndex = 0;\n          bytesUntilNextAtom = 16;\n          if (MP4_PARSER_DEBUG) System.out.println("Found sample-to-chunk atom");\n        }\n        else if ("stts".equals(type) && chunkQuality > 0)\n        {\n          int[] sttsData = new int[2 * (((remAtomData[12] & 0xFF) << 24) | ((remAtomData[13] & 0xFF) << 16) |\n              ((remAtomData[14] & 0xFF) << 8) | (remAtomData[15] & 0xFF))];\n          if (chunkQuality == 2)\n            sttsDataV = sttsData;\n          else\n            sttsDataA = sttsData;\n          sttsIndex = 0;\n          bytesUntilNextAtom = 16;\n          if (MP4_PARSER_DEBUG) System.out.println("Found time-to-sample atom");\n        }\n        else if ("stco".equals(type) && chunkQuality > 0)\n        {\n          // Parse this table so then we know what chunk # we are on for a given position in the stream (also handle \'co64\')\n          long[] chunkOffsets = new long[((remAtomData[12] & 0xFF) << 24) | ((remAtomData[13] & 0xFF) << 16) |\n                                         ((remAtomData[14] & 0xFF) << 8) | (remAtomData[15] & 0xFF)];\n          if (chunkQuality == 2)\n            chunkOffsetsV = chunkOffsets;\n          else\n            chunkOffsetsA = chunkOffsets;\n          stcoIndex = 0;\n          bytesUntilNextAtom = 16;\n          areOffsets64Bits = false;\n          if (MP4_PARSER_DEBUG) System.out.println("Found chunk offset atom");\n        }\n        else if ("co64".equals(type) && chunkQuality > 0)\n        {\n          // Parse this table so then we know what chunk # we are on for a given position in the stream (also handle \'co64\')\n          long[] chunkOffsets = new long[((remAtomData[12] & 0xFF) << 24) | ((remAtomData[13] & 0xFF) << 16) |\n                                         ((remAtomData[14] & 0xFF) << 8) | (remAtomData[15] & 0xFF)];\n          if (chunkQuality == 2)\n            chunkOffsetsV = chunkOffsets;\n          else\n            chunkOffsetsA = chunkOffsets;\n          stcoIndex = 0;\n          bytesUntilNextAtom = 16;\n          areOffsets64Bits = true;\n          if (MP4_PARSER_DEBUG) System.out.println("Found chunk offset (64-bit) atom");\n        }\n        else if ("mdat".equals(type))\n        {\n          if (chunkOffsetsV == null || mp4TimeIndexV == null)\n          {\n            if (Sage.DBG) System.out.println("MP4 file being downloaded is NOT STREAMBLE; the mdat section came before the sample tables");\n            if (circSize > 0)\n            {\n              lastDownloadTimestamp = 5000; // to make it think we\'ve got somewhere in the download\n              indexAtEndOfMP4 = true;\n              fullDownloadRequired = false;\n            }\n            else\n              indexAtEndOfMP4 = fullDownloadRequired = true;\n          }\n          movieDataBegan = true;\n        }\n        if ("trak".equals(type))\n          chunkQuality = -1; // signifies time to look for a new media type\n        if ("moov".equals(type) || "trak".equals(type) || "mdia".equals(type) || "minf".equals(type) || "stbl".equals(type)) // it contains nested atoms\n          bytesUntilNextAtom = 8;\n      }\n      if (bytesUntilNextAtom > 0)\n      {\n        if (remAtomSize > 0)\n        {\n          int used = Math.min((int)bytesUntilNextAtom, remAtomSize);\n          if (used != remAtomSize)\n          {\n            System.arraycopy(remAtomData, used, remAtomData, 0, remAtomSize - used);\n          }\n          remAtomSize -= used;\n          bytesUntilNextAtom -= used;\n        }\n        if (bytesUntilNextAtom > 0)\n        {\n          long used = Math.min(bytesUntilNextAtom, length);\n          length -= used;\n          offset += used;\n          bytesUntilNextAtom -= used;\n          if (length == 0)\n            return;\n        }\n      }\n    }\n    if (length > 0)\n    {\n      System.arraycopy(buf, offset, remAtomData, remAtomSize, length);\n      remAtomSize = length;\n    }\n  }\n\n  private boolean processMP3Packet(byte[] buf, int offset, int length)\n  {\n    int index = 0;\n    // scan through file to get syncword\n    int headerstring = ((buf[index + offset] << 16) & 0x00FF0000)\n        | ((buf[index+1+offset] << 8) & 0x0000FF00)\n        | ((buf[index+2+offset] << 0) & 0x000000FF);\n    boolean isSync = false;\n    index += 2;\n    do {\n      headerstring <<= 8;\n      index++;\n      if (index+offset >= length) {\n        // We didn\'t find the MP3 syncword so it probably isn\'t it\n        return false;\n      }\n\n      headerstring |= (buf[index+offset] & 0x000000FF);\n      isSync = ((headerstring & 0xFFE00000) == 0xFFE00000);\n      // MPEG 2.5\n      // filter out invalid sample rate\n      if (isSync)\n        isSync = (((headerstring >>> 10) & 3) != 3);\n      // filter out invalid layer\n      if (isSync)\n        isSync = (((headerstring >>> 17) & 3) != 0);\n      // filter out invalid version\n      if (isSync)\n        isSync = (((headerstring >>> 19) & 3) != 1);\n    } while (!isSync);\n    // Got a header... lets have a look at it...\n    int h_version = ((headerstring >>> 19) & 1);\n    if (((headerstring >>> 20) & 1) == 0) // SZD: MPEG2.5 detection\n      if (h_version == MP3Utils.MPEG2_LSF)\n        h_version = MP3Utils.MPEG25_LSF;\n      else {\n        if (Sage.DBG) System.out.println("invalid MPEG header (version)");\n        return false;\n      }\n    int h_sample_frequency;\n    if ((h_sample_frequency = ((headerstring >>> 10) & 3)) == 3) {\n      if (Sage.DBG) System.out.println("invalid MPEG header (sample freq)");\n      return false;\n    }\n    int h_layer = 4 - (headerstring >>> 17) & 3;\n    int h_bitrate_index = (headerstring >>> 12) & 0xF;\n    mp3Bitrate = MP3Utils.bitrates[h_version][h_layer - 1][h_bitrate_index];\n    if (Sage.DBG) System.out.println("Detected mp3 bitrate of:" + mp3Bitrate);\n    return true;\n  }\n\n  // NOTE: Not all MP4 content can be played back streaming. They may not have certain atom information\n  // at the front of the file that is necessary to decode before playback. We judge this based off\n  // the sample table. If we haven\'t hit it yet then we don\'t know if it\'s streamable. But we can tell if\n  // we see a very large atom which is going to take the majority of the file that we know it will not be\n  // streamable.\n\n  private void stvTaskRun()\n  {\n    downloadedBytes = 0;\n    if (xferBuf == null)\n      xferBuf = new byte[Sage.getInt("file_transfer_buffer_size", 65536)];\n    java.text.NumberFormat prctForm = java.text.NumberFormat.getPercentInstance();\n    if (abort)\n    {\n      // In case someone is viewing while we\'re downloading\n      Sage.processInactiveFile(myDestFile.toString());\n      return;\n    }\n    long totalFileSize = remoteSize;\n    try\n    {\n      long currOffset = 0;\n      while (totalFileSize > 0)\n      {\n        if (abort)\n        {\n          // In case someone is viewing while we\'re downloading\n          Sage.processInactiveFile(myDestFile.toString());\n          return;\n        }\n        int currRead = (int)Math.min(xferBuf.length, totalFileSize);\n        outStream.write(("READ " + currOffset + " " + currRead + "\\r\\n").getBytes(Sage.BYTE_CHARSET));\n        outStream.flush();\n        inStream.readFully(xferBuf, 0, currRead);\n        if (circSize > 0)\n        {\n          waitForCircWrite(currRead);\n          if (fileOut.position() + currRead < circSize)\n            fileOut.write(xferBuf, 0, currRead);\n          else\n          {\n            int firstWrite = (int)(circSize - fileOut.position());\n            fileOut.write(xferBuf, 0, firstWrite);\n            fileOut.seek(0);\n            fileOut.write(xferBuf, firstWrite, currRead - firstWrite);\n          }\n        }\n        else\n          fileOut.write(xferBuf, 0, currRead);\n        downloadedBytes += currRead;\n        statusMessage = prctForm.format(((double)downloadedBytes)/remoteSize);\n        totalFileSize -= currRead;\n        currOffset += currRead;\n      }\n      outStream.write("QUIT\\r\\n".getBytes(Sage.BYTE_CHARSET));\n      outStream.flush();\n    }\n    catch (Exception e)\n    {\n      if (Sage.DBG) System.out.println("ERROR during file download of:" + e);\n      Sage.printStackTrace(e);\n      statusMessage = "Error:" + e.toString();\n      return;\n    }\n    finally\n    {\n      try{\n        if (sock != null)\n          sock.close();\n      }catch (Exception e1){}\n      try{\n        if (outStream != null)\n          outStream.close();\n      }catch (Exception e2){}\n      try{\n        if (inStream != null)\n          inStream.close();\n      }catch (Exception e3){}\n      try{\n        if (fileOut != null)\n          fileOut.close();\n      }catch (Exception e4){}\n    }\n\n    succeeded();\n    // In case someone is viewing while we\'re downloading..not needed for synchronous transfers\n    if (!synchronous)\n      Sage.processInactiveFile(myDestFile.toString());\n  }\n\n  private void remoteUITaskRun()\n  {\n    downloadedBytes = 0;\n    java.text.NumberFormat prctForm = java.text.NumberFormat.getPercentInstance();\n    MiniClientSageRenderer.RemoteFSXfer fsXferOp = (MiniClientSageRenderer.RemoteFSXfer) fsXferOpWeak.get();\n    while (!fsXferOp.isDone())\n    {\n      if (abort)\n      {\n        fsXferOp.abortNow();\n        return;\n      }\n      try{Thread.sleep(250);}catch(Exception e){}\n      downloadedBytes = fsXferOp.getBytesXferd();\n      statusMessage = prctForm.format(((double)downloadedBytes)/remoteSize);\n    }\n\n    if (fsXferOp.error != 0)\n    {\n      statusMessage = "Error:" + fsXferOp.error;\n      return;\n    }\n    succeeded();\n  }\n\n  public long getNumDownloadedBytes()\n  {\n    return downloadedBytes;\n  }\n\n  public long getLastDownloadTimestamp()\n  {\n    return lastDownloadTimestamp;\n  }\n\n  public long getMinSeekTimestamp()\n  {\n    if (circSize > 0)\n    {\n      if (isMP4Stream && httpRangeSupported & !isComplete())\n        return 0; // We can seek anywhere in this case\n      else\n      {\n        // Only let them seek backwards into what we still have in the circular buffer. But this is\n        // tricky because we\'ll need to track timestamps for this. For now just guess based on avg. bitrate\n        // which we can easily get. And put the seek point 10% of the way into the buffer.\n        if (downloadedBytes < circSize)\n          return 0;\n        else\n        {\n          long minSeekPoint = Math.max(lastDownloadOffset, downloadedBytes - circSize) + circSize/10;\n          if (minSeekPoint >= downloadedBytes)\n            return lastDownloadTimestamp;\n          else\n          {\n            return lastDownloadTimestamp * minSeekPoint / downloadedBytes;\n          }\n        }\n      }\n    }\n    else\n      return 0; // the whole thing\'s being downloaded\n  }\n\n  public long getMaxSeekTimestamp()\n  {\n    if (circSize > 0 && isMP4Stream && httpRangeSupported)\n      return Long.MAX_VALUE/10; // avoid overflows when this is used in math outside of here\n    else\n      return lastDownloadTimestamp;\n  }\n\n  // Returns true if things interested in this downloaders timing progress need\n  // to make calls to find out more. (i.e. this is always true for circular files; but its false once a normal download completes)\n  public boolean needsTracking()\n  {\n    return circSize > 0 || !isComplete();\n  }\n\n  public boolean isProgressivePlay()\n  {\n    return !fullDownloadRequired;\n  }\n\n  public long getCircularDownloadSize()\n  {\n    return circSize;\n  }\n\n  // This call is used during progressive download playback w/ circular file storage.\n  // It\'s called so the downloader knows where the reading is currently at so it doesn\'t get too\n  // far ahead and wrap around and write over what hasn\'t been played yet\n  public void notifyReadRequest(long offset, long length)\n  {\n    lastNotifyReadOffset = offset;\n    lastNotifyReadLength = length;\n    if (CIRC_DOWNLOAD_DEBUG) System.out.println("notifyReadRequest=" + offset + " downloadedRange=" + Math.max(lastDownloadOffset, downloadedBytes - circSize) + " - " +\n        downloadedBytes);\n    if (circSize > 0 && !isComplete())\n    {\n      // Check to see if this read request is for data that\'s before what\'s in the buffer\n      if (offset < Math.max(lastDownloadOffset, downloadedBytes - circSize))\n      {\n        //System.out.println("WARNING : Attempting to read from offset before the buffer: offset=" + offset);\n        waitingForRead = true;\n        while (alive && !abort && offset < Math.max(lastDownloadOffset, downloadedBytes - circSize))\n        {\n          synchronized (notifyRead)\n          {\n            try\n            {\n              notifyRead.wait(30);\n            }\n            catch (InterruptedException e){ }\n          }\n        }\n        //System.out.println("DONE waiting for backwards read request to be ready!!");\n        waitingForRead = false;\n      }\n      // Check to see if this read request is for data that\'s after what\'s in the buffer\n      long maxRead = (remoteSize > 0) ? Math.min(remoteSize, offset + length) : (offset + length);\n      if (maxRead > downloadedBytes/* && isMP4Stream*/)\n      {\n        //System.out.println("WARNING: Trying to read data beyond what\'s been downloaded diff=" + ((maxRead) - downloadedBytes));\n        waitingForRead = true;\n        while (alive && !abort && maxRead > downloadedBytes)\n        {\n          synchronized (notifyRead)\n          {\n            try\n            {\n              notifyRead.wait(30);\n            }\n            catch (InterruptedException e){ }\n          }\n        }\n        //System.out.println("DONE waiting for forwards read request to be ready!!");\n        waitingForRead = false;\n      }\n      synchronized (notifyRead)\n      {\n        notifyRead.notify();\n        // Check if we need to flush this to disk yet in case the read from the circular file would be on data that\'s still\n        // in the write buffer for the RAF\n        if (offset + length > downloadedBytes - 65536)\n        {\n          try\n          {\n            if (fileOut != null)\n              fileOut.flush();\n          }\n          catch (Exception e){}\n        }\n      }\n    }\n  }\n\n  // Pauses and waits until it is OK to write data of the specified size to the circular file.\n  protected void waitForCircWrite(int writeSize)\n  {\n    if (circSize > 0 && (downloadedBytes + writeSize) >= circSize)\n    {\n      // We want the player to be in the middle of the circular buffer. So if they\'re at least 40% of the\n      // way into the buffer for their last read, we do this write.\n      long markerPoint = Math.max(0, downloadedBytes - circSize) + circSize * 4 / 10;\n      long minClientReadPoint = getSafeMP4SeekPositionForReadPosition(lastNotifyReadOffset, 0);\n      if (CIRC_DOWNLOAD_DEBUG && markerPoint > minClientReadPoint)\n        System.out.println("WAITING for circ buffer write to be OK");\n      while (alive && markerPoint > minClientReadPoint && !abort && !waitingForRead)\n      {\n        synchronized (notifyRead)\n        {\n          try\n          {\n            notifyRead.wait(50);\n          }\n          catch (InterruptedException e){ }\n        }\n      }\n    }\n  }\n\n  public boolean isClientWaitingForRead()\n  {\n    return waitingForRead;\n  }\n\n  public void notifyForPlayerSeek(long seekTimestamp)\n  {\n    if (CIRC_DOWNLOAD_DEBUG) System.out.println("Downloader notified of seek to: " + seekTimestamp);\n    lastSeekNotifyTimestamp = seekTimestamp;\n  }\n\n\n  public long getRemoteSize() { return remoteSize; }\n\n  //\tprivate static boolean addedBouncy = false;\n  private int decryptBuffer(byte[] xferBuf, int numRead, byte[] decryptBuf, boolean finalStep) throws Exception\n  {\n    // Here\'s where we can go to native code instead for the operation for performance reasons\n    if (decryptCipher == null)\n    {\n      decryptKey = new javax.crypto.spec.SecretKeySpec(cryptoInfo.key, cryptoInfo.algorithm.substring(0, cryptoInfo.algorithm.indexOf(\'/\')));\n      /*\t\t\tif (!addedBouncy)\n\t\t\t{\n\t\t\t\tjava.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());\n\t\t\t\taddedBouncy = true;\n\t\t\t}*/\n      decryptCipher = javax.crypto.Cipher.getInstance(cryptoInfo.algorithm);\n      if (cryptoInfo.iv != null)\n      {\n        javax.crypto.spec.IvParameterSpec ivSpec = new javax.crypto.spec.IvParameterSpec(cryptoInfo.iv);\n        decryptCipher.init(javax.crypto.Cipher.DECRYPT_MODE, decryptKey, ivSpec);\n      }\n      else\n        decryptCipher.init(javax.crypto.Cipher.DECRYPT_MODE, decryptKey);\n    }\n    if (finalStep)\n    {\n      return decryptCipher.doFinal(xferBuf, 0, numRead, decryptBuf);\n    }\n    else\n      return decryptCipher.update(xferBuf, 0, numRead, decryptBuf);\n  }\n\n  public static class DecryptInfo\n  {\n    public DecryptInfo(String algorithm, byte[] key, byte[] iv)\n    {\n      this.algorithm = algorithm;\n      this.key = key;\n      this.iv = iv;\n    }\n    public String algorithm;\n    public byte[] key;\n    public byte[] iv;\n  }\n\n  protected String myServerName;\n  protected java.io.File mySrcFile;\n  protected java.io.File myDestFile;\n  protected java.net.Socket sock = null;\n  protected java.io.DataOutputStream outStream = null;\n  protected java.io.DataInputStream inStream = null;\n  protected sage.io.SageFileSource fileOut = null;\n  protected long remoteSize;\n  protected boolean isMP4Stream;\n\n  protected java.net.URL myURL;\n  protected java.net.URLConnection myURLConn;\n  protected java.io.InputStream urlinStream = null;\n\n  protected long lastDownloadTimestamp;\n\n  protected boolean remoteUIXfer;\n\n  protected boolean gotMSAccess;\n  protected boolean gotSMBAccess;\n\n  protected boolean httpRangeSupported;\n\n  protected long downloadedBytes;\n  protected int mp3Bitrate;\n  protected boolean fullDownloadRequired;\n  protected boolean indexAtEndOfMP4;\n  protected long circSize;\n  protected long lastNotifyReadOffset;\n  protected long lastNotifyReadLength;\n  protected long lastDownloadOffset; // if we restart a download at a new offset, save it here\n  protected Object notifyRead = new Object();\n  protected boolean waitingForRead = false;\n  protected long lastAutoReseekTime;\n\n\n  // NOTE: Must use weak references here so we don\'t prevent GC of the UI stuff from\n  // being a value in the WeakHashMap (who\'s value then has a strong reference to us, and the WeakHashMap values are strong refs as well)\n  protected java.lang.ref.WeakReference uiMgrWeak;\n  protected java.lang.ref.WeakReference fsXferOpWeak;\n\n  protected boolean synchronous;\n  protected byte[] xferBuf;\n  protected boolean captureMode;\n\n  protected DecryptInfo cryptoInfo;\n  protected javax.crypto.Cipher decryptCipher;\n  protected javax.crypto.spec.SecretKeySpec decryptKey;\n}\n', 'message': "Merge pull request #10 from CraziFuzzy/master\n\nAdded property 'custom_user_agent' to override default.", 'author_name': 'Jeffrey Kardatzke', 'lang': 'java'}