| // |
| // Copyright (C) 2008 Wimba, Inc. All Rights Reserved. |
| // |
| // This is free software; you can redistribute it and/or modify |
| // it under the terms of the GNU General Public License as published by |
| // the Free Software Foundation; either version 2 of the License, or |
| // (at your option) any later version. |
| // |
| // This software is distributed in the hope that it will be useful, |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| // GNU General Public License for more details. |
| // |
| // You should have received a copy of the GNU General Public License |
| // along with this software; if not, write to the Free Software |
| // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
| // USA. |
| // |
| |
| // |
| // FbsConnection.java |
| // |
| |
| package com.tightvnc.rfbplayer; |
| |
| import java.io.*; |
| import java.net.*; |
| import java.applet.Applet; |
| |
| public class FbsConnection { |
| |
| URL fbsURL; |
| URL fbiURL; |
| URL fbkURL; |
| |
| /** Index data loaded from the .fbi file. */ |
| FbsEntryPoint[] indexData; |
| int numIndexRecords; |
| |
| /** RFB initialization data loaded from the .fbi file. */ |
| byte[] rfbInitData; |
| |
| FbsConnection(String fbsLocation, String indexLocationPrefix, Applet applet) |
| throws MalformedURLException { |
| |
| // Construct URLs from strings. |
| URL base = null; |
| if (applet != null) { |
| base = applet.getCodeBase(); |
| } |
| fbsURL = new URL(base, fbsLocation); |
| fbiURL = fbkURL = null; |
| if (indexLocationPrefix != null) { |
| try { |
| fbiURL = new URL(base, indexLocationPrefix + ".fbi"); |
| fbkURL = new URL(base, indexLocationPrefix + ".fbk"); |
| } catch (MalformedURLException e) { |
| fbiURL = fbkURL = null; |
| } |
| } |
| |
| // Try to load the .fbi index file. |
| indexData = null; |
| numIndexRecords = 0; |
| rfbInitData = null; |
| loadIndex(); |
| } |
| |
| FbsInputStream connect(long timeOffset) throws IOException { |
| FbsInputStream fbs = null; |
| |
| // Try efficient seeking first. |
| int i = indexForTimeOffset(timeOffset); |
| if (i >= 0) { |
| FbsEntryPoint entryPoint = indexData[i]; |
| if (entryPoint.key_size < entryPoint.fbs_fpos) { |
| try { |
| fbs = openFbsFile(entryPoint); |
| } catch (IOException e) { |
| System.err.println(e); |
| } |
| if (fbs == null) { |
| System.err.println("Could not open FBS file at entry point " + |
| entryPoint.timestamp + " ms"); |
| } |
| } |
| } |
| |
| // Fallback to the dumb version of openFbsFile(). |
| if (fbs == null) { |
| fbs = openFbsFile(); |
| } |
| |
| // Seek to the specified position. |
| fbs.setTimeOffset(timeOffset, false); |
| return fbs; |
| } |
| |
| /** |
| * Load index data from .fbi file to {@link #indexData}. |
| */ |
| private void loadIndex() { |
| // Loading .fbi makes sense only if both .fbi and .fbk files are available. |
| if (fbiURL != null && fbkURL != null) { |
| FbsEntryPoint[] newIndex; |
| int numRecordsRead = 0; |
| byte[] newInitData; |
| try { |
| // Connect. |
| URLConnection connection = fbiURL.openConnection(); |
| connection.connect(); |
| DataInputStream is = new DataInputStream(connection.getInputStream()); |
| |
| // Check file signature. |
| byte[] b = new byte[12]; |
| is.readFully(b); |
| if (b[0] != 'F' || b[1] != 'B' || b[2] != 'I' || b[3] != ' ' || |
| b[4] != '0' || b[5] != '0' || b[6] != '1' || b[7] != '.' || |
| b[8] < '0' || b[8] > '9' || b[9] < '0' || b[9] > '9' || |
| b[10] < '0' || b[10] > '9' || b[11] != '\n') { |
| System.err.println("Could not load index: bad .fbi file signature"); |
| return; |
| } |
| |
| // Read the record counter and allocate index array. |
| int numRecords = is.readInt(); |
| if (numRecords <= 0) { |
| System.err.println("Could not load index: bad .fbi record counter"); |
| return; |
| } |
| newIndex = new FbsEntryPoint[numRecords]; |
| |
| // Read byte counter and allocate byte array for RFB initialization. |
| int initSize = is.readInt(); |
| if (initSize <= 0) { |
| System.err.println("Could not load index: bad RFB init data size"); |
| return; |
| } |
| newInitData = new byte[initSize]; |
| |
| // Load index from the .fbi file. |
| try { |
| for (int i = 0; i < numRecords; i++) { |
| FbsEntryPoint record = new FbsEntryPoint(); |
| record.timestamp = (long)is.readInt() & 0xFFFFFFFFL; |
| record.key_fpos = (long)is.readInt() & 0xFFFFFFFFL; |
| record.key_size = (long)is.readInt() & 0xFFFFFFFFL; |
| record.fbs_fpos = (long)is.readInt() & 0xFFFFFFFFL; |
| record.fbs_skip = (long)is.readInt() & 0xFFFFFFFFL; |
| newIndex[i] = record; |
| numRecordsRead++; |
| } |
| } catch (EOFException e) { |
| System.err.println("Preliminary end of .fbi file"); |
| } catch (IOException e) { |
| System.err.println("Ignored exception: " + e); |
| } |
| if (numRecordsRead == 0) { |
| System.err.println("Could not load index: failed to read .fbi data"); |
| return; |
| } else if (numRecordsRead != numRecords) { |
| System.err.println("Warning: read not as much .fbi data as expected"); |
| } |
| is.readFully(newInitData); |
| } catch (FileNotFoundException e) { |
| System.err.println("Could not load index: .fbi file not found: " + |
| e.getMessage()); |
| return; |
| } catch (IOException e) { |
| System.err.println(e); |
| System.err.println("Could not load index: failed to load .fbi file"); |
| return; |
| } |
| // Check correctness of the data read. |
| for (int i = 1; i < numRecordsRead; i++) { |
| if (newIndex[i].timestamp <= newIndex[i - 1].timestamp) { |
| System.err.println("Could not load index: wrong .fbi file contents"); |
| return; |
| } |
| } |
| // Loaded successfully. |
| indexData = newIndex; |
| numIndexRecords = numRecordsRead; |
| rfbInitData = newInitData; |
| System.err.println("Loaded index data, " + numRecordsRead + " records"); |
| } |
| } |
| |
| private int indexForTimeOffset(long timeOffset) { |
| if (timeOffset > 0 && indexData != null && numIndexRecords > 0) { |
| int i = 0; |
| while (i < numIndexRecords && indexData[i].timestamp <= timeOffset) { |
| i++; |
| } |
| return i - 1; |
| } else { |
| return -1; |
| } |
| } |
| |
| /** |
| * Open FBS file identified by {@link #fbsURL}. The file is open at its very |
| * beginning, no seek is performed. |
| * |
| * @return a newly created FBS input stream. |
| * @throws java.io.IOException if an I/O exception occurs. |
| */ |
| private FbsInputStream openFbsFile() throws IOException { |
| return new FbsInputStream(fbsURL.openStream()); |
| } |
| |
| /** |
| * Open FBS file identified by {@link #fbsURL}. The stream is |
| * positioned at the entry point described by <code>entryPoint</code>. |
| * |
| * @param entryPoint entry point information. |
| * |
| * @return a newly created FBS input stream on success, <code>null</code> if |
| * any error occured and the FBS stream is not opened. |
| * @throws java.io.IOException if an I/O exception occurs. |
| */ |
| private FbsInputStream openFbsFile(FbsEntryPoint entry) |
| throws IOException { |
| |
| System.err.println("Entering FBS at " + entry.timestamp + " ms"); |
| |
| // Make sure the protocol is HTTP. |
| if (!fbkURL.getProtocol().equalsIgnoreCase("http") || |
| !fbsURL.getProtocol().equalsIgnoreCase("http")) { |
| System.err.println("Indexed access requires HTTP protocol in URLs"); |
| return null; |
| } |
| |
| // Seek to the keyframe. |
| InputStream is = openHttpByteRange(fbkURL, entry.key_fpos, entry.key_size); |
| if (is == null) { |
| return null; |
| } |
| |
| // Load keyframe data from the .fbk file, prepend RFB initialization data. |
| DataInputStream data = new DataInputStream(is); |
| byte[] keyData = new byte[rfbInitData.length + (int)entry.key_size]; |
| System.arraycopy(rfbInitData, 0, keyData, 0, rfbInitData.length); |
| data.readFully(keyData, rfbInitData.length, (int)entry.key_size); |
| data.close(); |
| |
| // Open the FBS stream. |
| is = openHttpByteRange(fbsURL, entry.fbs_fpos, -1); |
| if (is == null) { |
| return null; |
| } |
| return new FbsInputStream(is, entry.timestamp, keyData, entry.fbs_skip); |
| } |
| |
| private static InputStream openHttpByteRange(URL url, long offset, long len) |
| throws IOException { |
| HttpURLConnection conn = (HttpURLConnection)url.openConnection(); |
| String rangeSpec = "bytes=" + offset + "-"; |
| if (len != -1) { |
| long lastByteOffset = offset + len - 1; |
| rangeSpec += lastByteOffset; |
| } |
| conn.setRequestProperty("Range", rangeSpec); |
| conn.connect(); |
| InputStream is = conn.getInputStream(); |
| if (conn.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) { |
| System.err.println("HTTP server does not support Range request headers"); |
| is.close(); |
| return null; |
| } |
| return is; |
| } |
| |
| } |