Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 1 | // |
| 2 | // Copyright (C) 2008 Wimba, Inc. All Rights Reserved. |
| 3 | // |
| 4 | // This is free software; you can redistribute it and/or modify |
| 5 | // it under the terms of the GNU General Public License as published by |
| 6 | // the Free Software Foundation; either version 2 of the License, or |
| 7 | // (at your option) any later version. |
| 8 | // |
| 9 | // This software is distributed in the hope that it will be useful, |
| 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | // GNU General Public License for more details. |
| 13 | // |
| 14 | // You should have received a copy of the GNU General Public License |
| 15 | // along with this software; if not, write to the Free Software |
| 16 | // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
| 17 | // USA. |
| 18 | // |
| 19 | |
| 20 | // |
| 21 | // FbsConnection.java |
| 22 | // |
| 23 | |
| 24 | package com.tightvnc.rfbplayer; |
| 25 | |
| 26 | import java.io.*; |
| 27 | import java.net.*; |
| 28 | import java.applet.Applet; |
| 29 | |
| 30 | public class FbsConnection { |
| 31 | |
| 32 | URL fbsURL; |
| 33 | URL fbiURL; |
| 34 | URL fbkURL; |
| 35 | |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 36 | /** Index data loaded from the .fbi file. */ |
Constantin Kaplinsky | 5ba167a | 2008-06-20 12:24:52 +0000 | [diff] [blame] | 37 | FbsEntryPoint[] indexData; |
Constantin Kaplinsky | be68e7f | 2008-06-20 12:21:57 +0000 | [diff] [blame] | 38 | int numIndexRecords; |
Constantin Kaplinsky | 7354012 | 2008-06-23 12:30:41 +0000 | [diff] [blame] | 39 | |
Constantin Kaplinsky | 9e50164 | 2008-06-23 01:57:55 +0000 | [diff] [blame] | 40 | /** RFB initialization data loaded from the .fbi file. */ |
| 41 | byte[] rfbInitData; |
Constantin Kaplinsky | ade425a | 2008-06-20 06:44:03 +0000 | [diff] [blame] | 42 | |
Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 43 | FbsConnection(String fbsLocation, String indexLocationPrefix, Applet applet) |
| 44 | throws MalformedURLException { |
| 45 | |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 46 | // Construct URLs from strings. |
Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 47 | URL base = null; |
| 48 | if (applet != null) { |
| 49 | base = applet.getCodeBase(); |
| 50 | } |
| 51 | fbsURL = new URL(base, fbsLocation); |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 52 | fbiURL = fbkURL = null; |
Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 53 | if (indexLocationPrefix != null) { |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 54 | try { |
| 55 | fbiURL = new URL(base, indexLocationPrefix + ".fbi"); |
| 56 | fbkURL = new URL(base, indexLocationPrefix + ".fbk"); |
| 57 | } catch (MalformedURLException e) { |
| 58 | fbiURL = fbkURL = null; |
| 59 | } |
Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 60 | } |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 61 | |
| 62 | // Try to load the .fbi index file. |
Constantin Kaplinsky | 5ba167a | 2008-06-20 12:24:52 +0000 | [diff] [blame] | 63 | indexData = null; |
Constantin Kaplinsky | be68e7f | 2008-06-20 12:21:57 +0000 | [diff] [blame] | 64 | numIndexRecords = 0; |
Constantin Kaplinsky | 9e50164 | 2008-06-23 01:57:55 +0000 | [diff] [blame] | 65 | rfbInitData = null; |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 66 | loadIndex(); |
Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 67 | } |
| 68 | |
| 69 | FbsInputStream connect(long timeOffset) throws IOException { |
Constantin Kaplinsky | 1c2865f | 2008-06-20 18:50:27 +0000 | [diff] [blame] | 70 | FbsInputStream fbs = null; |
| 71 | |
Constantin Kaplinsky | 38f044f | 2008-06-20 12:57:37 +0000 | [diff] [blame] | 72 | // Try efficient seeking first. |
Constantin Kaplinsky | f1707d7 | 2008-06-23 13:01:11 +0000 | [diff] [blame] | 73 | int i = indexForTimeOffset(timeOffset); |
| 74 | if (i >= 0) { |
| 75 | FbsEntryPoint entryPoint = indexData[i]; |
| 76 | if (entryPoint.key_size < entryPoint.fbs_fpos) { |
| 77 | try { |
| 78 | fbs = openFbsFile(entryPoint); |
| 79 | } catch (IOException e) { |
| 80 | System.err.println(e); |
| 81 | } |
| 82 | if (fbs == null) { |
| 83 | System.err.println("Could not open FBS file at entry point " + |
| 84 | entryPoint.timestamp + " ms"); |
Constantin Kaplinsky | 38f044f | 2008-06-20 12:57:37 +0000 | [diff] [blame] | 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
Constantin Kaplinsky | 1c2865f | 2008-06-20 18:50:27 +0000 | [diff] [blame] | 89 | // Fallback to the dumb version of openFbsFile(). |
| 90 | if (fbs == null) { |
| 91 | fbs = openFbsFile(); |
| 92 | } |
Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 93 | |
Constantin Kaplinsky | 1c2865f | 2008-06-20 18:50:27 +0000 | [diff] [blame] | 94 | // Seek to the specified position. |
| 95 | fbs.setTimeOffset(timeOffset); |
Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 96 | return fbs; |
| 97 | } |
| 98 | |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 99 | /** |
Constantin Kaplinsky | 1c2865f | 2008-06-20 18:50:27 +0000 | [diff] [blame] | 100 | * Load index data from .fbi file to {@link #indexData}. |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 101 | */ |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 102 | private void loadIndex() { |
| 103 | // Loading .fbi makes sense only if both .fbi and .fbk files are available. |
| 104 | if (fbiURL != null && fbkURL != null) { |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 105 | FbsEntryPoint[] newIndex; |
| 106 | int numRecordsRead = 0; |
Constantin Kaplinsky | 9e50164 | 2008-06-23 01:57:55 +0000 | [diff] [blame] | 107 | byte[] newInitData; |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 108 | try { |
| 109 | // Connect. |
| 110 | URLConnection connection = fbiURL.openConnection(); |
| 111 | connection.connect(); |
| 112 | DataInputStream is = new DataInputStream(connection.getInputStream()); |
| 113 | |
| 114 | // Check file signature. |
| 115 | byte[] b = new byte[12]; |
| 116 | is.readFully(b); |
| 117 | if (b[0] != 'F' || b[1] != 'B' || b[2] != 'I' || b[3] != ' ' || |
| 118 | b[4] != '0' || b[5] != '0' || b[6] != '1' || b[7] != '.' || |
| 119 | b[8] < '0' || b[8] > '9' || b[9] < '0' || b[9] > '9' || |
| 120 | b[10] < '0' || b[10] > '9' || b[11] != '\n') { |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 121 | System.err.println("Could not load index: bad .fbi file signature"); |
| 122 | return; |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 123 | } |
| 124 | |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 125 | // Read the record counter and allocate index array. |
| 126 | int numRecords = is.readInt(); |
| 127 | if (numRecords <= 0) { |
| 128 | System.err.println("Could not load index: bad .fbi record counter"); |
| 129 | return; |
| 130 | } |
| 131 | newIndex = new FbsEntryPoint[numRecords]; |
| 132 | |
Constantin Kaplinsky | 9e50164 | 2008-06-23 01:57:55 +0000 | [diff] [blame] | 133 | // Read byte counter and allocate byte array for RFB initialization. |
| 134 | int initSize = is.readInt(); |
| 135 | if (initSize <= 0) { |
| 136 | System.err.println("Could not load index: bad RFB init data size"); |
| 137 | return; |
| 138 | } |
| 139 | newInitData = new byte[initSize]; |
| 140 | |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 141 | // Load index from the .fbi file. |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 142 | try { |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 143 | for (int i = 0; i < numRecords; i++) { |
| 144 | FbsEntryPoint record = new FbsEntryPoint(); |
| 145 | record.timestamp = (long)is.readInt() & 0xFFFFFFFFL; |
| 146 | record.key_fpos = (long)is.readInt() & 0xFFFFFFFFL; |
| 147 | record.key_size = (long)is.readInt() & 0xFFFFFFFFL; |
| 148 | record.fbs_fpos = (long)is.readInt() & 0xFFFFFFFFL; |
| 149 | record.fbs_skip = (long)is.readInt() & 0xFFFFFFFFL; |
| 150 | newIndex[i] = record; |
| 151 | numRecordsRead++; |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 152 | } |
| 153 | } catch (EOFException e) { |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 154 | System.err.println("Preliminary end of .fbi file"); |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 155 | } catch (IOException e) { |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 156 | System.err.println("Ignored exception: " + e); |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 157 | } |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 158 | if (numRecordsRead == 0) { |
| 159 | System.err.println("Could not load index: failed to read .fbi data"); |
| 160 | return; |
| 161 | } else if (numRecordsRead != numRecords) { |
| 162 | System.err.println("Warning: read not as much .fbi data as expected"); |
| 163 | } |
Constantin Kaplinsky | 9e50164 | 2008-06-23 01:57:55 +0000 | [diff] [blame] | 164 | is.readFully(newInitData); |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 165 | } catch (FileNotFoundException e) { |
| 166 | System.err.println("Could not load index: .fbi file not found: " + |
| 167 | e.getMessage()); |
| 168 | return; |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 169 | } catch (IOException e) { |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 170 | System.err.println(e); |
| 171 | System.err.println("Could not load index: failed to load .fbi file"); |
| 172 | return; |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 173 | } |
Constantin Kaplinsky | 4f39f8e | 2008-06-23 01:32:53 +0000 | [diff] [blame] | 174 | // Check correctness of the data read. |
| 175 | for (int i = 1; i < numRecordsRead; i++) { |
Constantin Kaplinsky | 7354012 | 2008-06-23 12:30:41 +0000 | [diff] [blame] | 176 | if (newIndex[i].timestamp <= newIndex[i - 1].timestamp) { |
Constantin Kaplinsky | 4f39f8e | 2008-06-23 01:32:53 +0000 | [diff] [blame] | 177 | System.err.println("Could not load index: wrong .fbi file contents"); |
| 178 | return; |
| 179 | } |
| 180 | } |
| 181 | // Loaded successfully. |
Constantin Kaplinsky | 5ba167a | 2008-06-20 12:24:52 +0000 | [diff] [blame] | 182 | indexData = newIndex; |
Constantin Kaplinsky | be68e7f | 2008-06-20 12:21:57 +0000 | [diff] [blame] | 183 | numIndexRecords = numRecordsRead; |
Constantin Kaplinsky | 9e50164 | 2008-06-23 01:57:55 +0000 | [diff] [blame] | 184 | rfbInitData = newInitData; |
Constantin Kaplinsky | f338d2c | 2008-06-20 11:53:19 +0000 | [diff] [blame] | 185 | System.err.println("Loaded index data, " + numRecordsRead + " records"); |
Constantin Kaplinsky | 5f1f886 | 2008-06-20 05:17:39 +0000 | [diff] [blame] | 186 | } |
| 187 | } |
| 188 | |
Constantin Kaplinsky | f1707d7 | 2008-06-23 13:01:11 +0000 | [diff] [blame] | 189 | private int indexForTimeOffset(long timeOffset) { |
| 190 | if (timeOffset > 0 && indexData != null && numIndexRecords > 0) { |
| 191 | int i = 0; |
| 192 | while (i < numIndexRecords && indexData[i].timestamp <= timeOffset) { |
| 193 | i++; |
| 194 | } |
| 195 | return i - 1; |
| 196 | } else { |
| 197 | return -1; |
| 198 | } |
| 199 | } |
| 200 | |
Constantin Kaplinsky | 1c2865f | 2008-06-20 18:50:27 +0000 | [diff] [blame] | 201 | /** |
| 202 | * Open FBS file identified by {@link #fbsURL}. The file is open at its very |
| 203 | * beginning, no seek is performed. |
| 204 | * |
| 205 | * @return a newly created FBS input stream. |
| 206 | * @throws java.io.IOException if an I/O exception occurs. |
| 207 | */ |
| 208 | private FbsInputStream openFbsFile() throws IOException { |
| 209 | return new FbsInputStream(fbsURL.openStream()); |
| 210 | } |
| 211 | |
| 212 | /** |
| 213 | * Open FBS file identified by {@link #fbsURL}. The stream is |
| 214 | * positioned at the entry point described by <code>entryPoint</code>. |
| 215 | * |
| 216 | * @param entryPoint entry point information. |
| 217 | * |
| 218 | * @return a newly created FBS input stream on success, <code>null</code> if |
| 219 | * any error occured and the FBS stream is not opened. |
| 220 | * @throws java.io.IOException if an I/O exception occurs. |
| 221 | */ |
Constantin Kaplinsky | a9bb64f | 2008-06-23 08:59:20 +0000 | [diff] [blame] | 222 | private FbsInputStream openFbsFile(FbsEntryPoint entry) |
Constantin Kaplinsky | 1c2865f | 2008-06-20 18:50:27 +0000 | [diff] [blame] | 223 | throws IOException { |
Constantin Kaplinsky | d3a2df1 | 2008-06-20 20:12:22 +0000 | [diff] [blame] | 224 | |
Constantin Kaplinsky | b29bb75 | 2008-06-23 13:26:17 +0000 | [diff] [blame] | 225 | System.err.println("Entering FBS at " + entry.timestamp + " ms"); |
| 226 | |
Constantin Kaplinsky | d3a2df1 | 2008-06-20 20:12:22 +0000 | [diff] [blame] | 227 | // Make sure the protocol is HTTP. |
| 228 | if (!fbkURL.getProtocol().equalsIgnoreCase("http") || |
| 229 | !fbsURL.getProtocol().equalsIgnoreCase("http")) { |
Constantin Kaplinsky | 31b7a24 | 2008-06-22 07:24:11 +0000 | [diff] [blame] | 230 | System.err.println("Indexed access requires HTTP protocol in URLs"); |
Constantin Kaplinsky | d3a2df1 | 2008-06-20 20:12:22 +0000 | [diff] [blame] | 231 | return null; |
| 232 | } |
| 233 | |
Constantin Kaplinsky | 31b7a24 | 2008-06-22 07:24:11 +0000 | [diff] [blame] | 234 | // Seek to the keyframe. |
Constantin Kaplinsky | a9bb64f | 2008-06-23 08:59:20 +0000 | [diff] [blame] | 235 | InputStream is = openHttpByteRange(fbkURL, entry.key_fpos, entry.key_size); |
Constantin Kaplinsky | f5821e5 | 2008-06-23 11:38:51 +0000 | [diff] [blame] | 236 | if (is == null) { |
| 237 | return null; |
| 238 | } |
Constantin Kaplinsky | 31b7a24 | 2008-06-22 07:24:11 +0000 | [diff] [blame] | 239 | |
Constantin Kaplinsky | 9e50164 | 2008-06-23 01:57:55 +0000 | [diff] [blame] | 240 | // Load keyframe data from the .fbk file, prepend RFB initialization data. |
Constantin Kaplinsky | f5821e5 | 2008-06-23 11:38:51 +0000 | [diff] [blame] | 241 | DataInputStream data = new DataInputStream(is); |
Constantin Kaplinsky | a9bb64f | 2008-06-23 08:59:20 +0000 | [diff] [blame] | 242 | byte[] keyData = new byte[rfbInitData.length + (int)entry.key_size]; |
Constantin Kaplinsky | 9e50164 | 2008-06-23 01:57:55 +0000 | [diff] [blame] | 243 | System.arraycopy(rfbInitData, 0, keyData, 0, rfbInitData.length); |
Constantin Kaplinsky | a9bb64f | 2008-06-23 08:59:20 +0000 | [diff] [blame] | 244 | data.readFully(keyData, rfbInitData.length, (int)entry.key_size); |
| 245 | data.close(); |
Constantin Kaplinsky | d3a2df1 | 2008-06-20 20:12:22 +0000 | [diff] [blame] | 246 | |
| 247 | // Open the FBS stream. |
Constantin Kaplinsky | a9bb64f | 2008-06-23 08:59:20 +0000 | [diff] [blame] | 248 | is = openHttpByteRange(fbsURL, entry.fbs_fpos, -1); |
Constantin Kaplinsky | f5821e5 | 2008-06-23 11:38:51 +0000 | [diff] [blame] | 249 | if (is == null) { |
| 250 | return null; |
| 251 | } |
Constantin Kaplinsky | a9bb64f | 2008-06-23 08:59:20 +0000 | [diff] [blame] | 252 | return new FbsInputStream(is, entry.timestamp, keyData, entry.fbs_skip); |
Constantin Kaplinsky | 1c2865f | 2008-06-20 18:50:27 +0000 | [diff] [blame] | 253 | } |
| 254 | |
Constantin Kaplinsky | 31b7a24 | 2008-06-22 07:24:11 +0000 | [diff] [blame] | 255 | private static InputStream openHttpByteRange(URL url, long offset, long len) |
Constantin Kaplinsky | d2af082 | 2008-06-21 11:41:42 +0000 | [diff] [blame] | 256 | throws IOException { |
Constantin Kaplinsky | 31b7a24 | 2008-06-22 07:24:11 +0000 | [diff] [blame] | 257 | HttpURLConnection conn = (HttpURLConnection)url.openConnection(); |
| 258 | String rangeSpec = "bytes=" + offset + "-"; |
| 259 | if (len != -1) { |
| 260 | long lastByteOffset = offset + len - 1; |
| 261 | rangeSpec += lastByteOffset; |
| 262 | } |
Constantin Kaplinsky | d2af082 | 2008-06-21 11:41:42 +0000 | [diff] [blame] | 263 | conn.setRequestProperty("Range", rangeSpec); |
| 264 | conn.connect(); |
Constantin Kaplinsky | 31b7a24 | 2008-06-22 07:24:11 +0000 | [diff] [blame] | 265 | InputStream is = conn.getInputStream(); |
| 266 | if (conn.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) { |
| 267 | System.err.println("HTTP server does not support Range request headers"); |
| 268 | is.close(); |
| 269 | return null; |
| 270 | } |
| 271 | return is; |
Constantin Kaplinsky | d2af082 | 2008-06-21 11:41:42 +0000 | [diff] [blame] | 272 | } |
| 273 | |
Constantin Kaplinsky | 2859fdc | 2008-06-19 16:07:52 +0000 | [diff] [blame] | 274 | } |