blob: 204f4ae34b32956c16b11cf824d65738e410aaae [file] [log] [blame]
Constantin Kaplinsky2859fdc2008-06-19 16:07:52 +00001//
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
24package com.tightvnc.rfbplayer;
25
26import java.io.*;
27import java.net.*;
28import java.applet.Applet;
29
30public class FbsConnection {
31
32 URL fbsURL;
33 URL fbiURL;
34 URL fbkURL;
35
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +000036 /** Index data loaded from the .fbi file. */
Constantin Kaplinsky5ba167a2008-06-20 12:24:52 +000037 FbsEntryPoint[] indexData;
Constantin Kaplinskybe68e7f2008-06-20 12:21:57 +000038 int numIndexRecords;
Constantin Kaplinsky73540122008-06-23 12:30:41 +000039
Constantin Kaplinsky9e501642008-06-23 01:57:55 +000040 /** RFB initialization data loaded from the .fbi file. */
41 byte[] rfbInitData;
Constantin Kaplinskyade425a2008-06-20 06:44:03 +000042
Constantin Kaplinsky2859fdc2008-06-19 16:07:52 +000043 FbsConnection(String fbsLocation, String indexLocationPrefix, Applet applet)
44 throws MalformedURLException {
45
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +000046 // Construct URLs from strings.
Constantin Kaplinsky2859fdc2008-06-19 16:07:52 +000047 URL base = null;
48 if (applet != null) {
49 base = applet.getCodeBase();
50 }
51 fbsURL = new URL(base, fbsLocation);
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +000052 fbiURL = fbkURL = null;
Constantin Kaplinsky2859fdc2008-06-19 16:07:52 +000053 if (indexLocationPrefix != null) {
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +000054 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 Kaplinsky2859fdc2008-06-19 16:07:52 +000060 }
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +000061
62 // Try to load the .fbi index file.
Constantin Kaplinsky5ba167a2008-06-20 12:24:52 +000063 indexData = null;
Constantin Kaplinskybe68e7f2008-06-20 12:21:57 +000064 numIndexRecords = 0;
Constantin Kaplinsky9e501642008-06-23 01:57:55 +000065 rfbInitData = null;
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +000066 loadIndex();
Constantin Kaplinsky2859fdc2008-06-19 16:07:52 +000067 }
68
69 FbsInputStream connect(long timeOffset) throws IOException {
Constantin Kaplinsky1c2865f2008-06-20 18:50:27 +000070 FbsInputStream fbs = null;
71
Constantin Kaplinsky38f044f2008-06-20 12:57:37 +000072 // Try efficient seeking first.
Constantin Kaplinskyf1707d72008-06-23 13:01:11 +000073 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 Kaplinsky38f044f2008-06-20 12:57:37 +000085 }
86 }
87 }
88
Constantin Kaplinsky1c2865f2008-06-20 18:50:27 +000089 // Fallback to the dumb version of openFbsFile().
90 if (fbs == null) {
91 fbs = openFbsFile();
92 }
Constantin Kaplinsky2859fdc2008-06-19 16:07:52 +000093
Constantin Kaplinsky1c2865f2008-06-20 18:50:27 +000094 // Seek to the specified position.
95 fbs.setTimeOffset(timeOffset);
Constantin Kaplinsky2859fdc2008-06-19 16:07:52 +000096 return fbs;
97 }
98
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +000099 /**
Constantin Kaplinsky1c2865f2008-06-20 18:50:27 +0000100 * Load index data from .fbi file to {@link #indexData}.
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000101 */
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000102 private void loadIndex() {
103 // Loading .fbi makes sense only if both .fbi and .fbk files are available.
104 if (fbiURL != null && fbkURL != null) {
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000105 FbsEntryPoint[] newIndex;
106 int numRecordsRead = 0;
Constantin Kaplinsky9e501642008-06-23 01:57:55 +0000107 byte[] newInitData;
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000108 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 Kaplinskyf338d2c2008-06-20 11:53:19 +0000121 System.err.println("Could not load index: bad .fbi file signature");
122 return;
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000123 }
124
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000125 // 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 Kaplinsky9e501642008-06-23 01:57:55 +0000133 // 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 Kaplinsky5f1f8862008-06-20 05:17:39 +0000141 // Load index from the .fbi file.
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000142 try {
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000143 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 Kaplinsky5f1f8862008-06-20 05:17:39 +0000152 }
153 } catch (EOFException e) {
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000154 System.err.println("Preliminary end of .fbi file");
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000155 } catch (IOException e) {
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000156 System.err.println("Ignored exception: " + e);
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000157 }
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000158 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 Kaplinsky9e501642008-06-23 01:57:55 +0000164 is.readFully(newInitData);
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000165 } catch (FileNotFoundException e) {
166 System.err.println("Could not load index: .fbi file not found: " +
167 e.getMessage());
168 return;
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000169 } catch (IOException e) {
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000170 System.err.println(e);
171 System.err.println("Could not load index: failed to load .fbi file");
172 return;
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000173 }
Constantin Kaplinsky4f39f8e2008-06-23 01:32:53 +0000174 // Check correctness of the data read.
175 for (int i = 1; i < numRecordsRead; i++) {
Constantin Kaplinsky73540122008-06-23 12:30:41 +0000176 if (newIndex[i].timestamp <= newIndex[i - 1].timestamp) {
Constantin Kaplinsky4f39f8e2008-06-23 01:32:53 +0000177 System.err.println("Could not load index: wrong .fbi file contents");
178 return;
179 }
180 }
181 // Loaded successfully.
Constantin Kaplinsky5ba167a2008-06-20 12:24:52 +0000182 indexData = newIndex;
Constantin Kaplinskybe68e7f2008-06-20 12:21:57 +0000183 numIndexRecords = numRecordsRead;
Constantin Kaplinsky9e501642008-06-23 01:57:55 +0000184 rfbInitData = newInitData;
Constantin Kaplinskyf338d2c2008-06-20 11:53:19 +0000185 System.err.println("Loaded index data, " + numRecordsRead + " records");
Constantin Kaplinsky5f1f8862008-06-20 05:17:39 +0000186 }
187 }
188
Constantin Kaplinskyf1707d72008-06-23 13:01:11 +0000189 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 Kaplinsky1c2865f2008-06-20 18:50:27 +0000201 /**
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 Kaplinskya9bb64f2008-06-23 08:59:20 +0000222 private FbsInputStream openFbsFile(FbsEntryPoint entry)
Constantin Kaplinsky1c2865f2008-06-20 18:50:27 +0000223 throws IOException {
Constantin Kaplinskyd3a2df12008-06-20 20:12:22 +0000224
225 // Make sure the protocol is HTTP.
226 if (!fbkURL.getProtocol().equalsIgnoreCase("http") ||
227 !fbsURL.getProtocol().equalsIgnoreCase("http")) {
Constantin Kaplinsky31b7a242008-06-22 07:24:11 +0000228 System.err.println("Indexed access requires HTTP protocol in URLs");
Constantin Kaplinskyd3a2df12008-06-20 20:12:22 +0000229 return null;
230 }
231
Constantin Kaplinsky31b7a242008-06-22 07:24:11 +0000232 // Seek to the keyframe.
Constantin Kaplinskya9bb64f2008-06-23 08:59:20 +0000233 InputStream is = openHttpByteRange(fbkURL, entry.key_fpos, entry.key_size);
Constantin Kaplinskyf5821e52008-06-23 11:38:51 +0000234 if (is == null) {
235 return null;
236 }
Constantin Kaplinsky31b7a242008-06-22 07:24:11 +0000237
Constantin Kaplinsky9e501642008-06-23 01:57:55 +0000238 // Load keyframe data from the .fbk file, prepend RFB initialization data.
Constantin Kaplinskyf5821e52008-06-23 11:38:51 +0000239 DataInputStream data = new DataInputStream(is);
Constantin Kaplinskya9bb64f2008-06-23 08:59:20 +0000240 byte[] keyData = new byte[rfbInitData.length + (int)entry.key_size];
Constantin Kaplinsky9e501642008-06-23 01:57:55 +0000241 System.arraycopy(rfbInitData, 0, keyData, 0, rfbInitData.length);
Constantin Kaplinskya9bb64f2008-06-23 08:59:20 +0000242 data.readFully(keyData, rfbInitData.length, (int)entry.key_size);
243 data.close();
Constantin Kaplinskyd3a2df12008-06-20 20:12:22 +0000244
245 // Open the FBS stream.
Constantin Kaplinskya9bb64f2008-06-23 08:59:20 +0000246 is = openHttpByteRange(fbsURL, entry.fbs_fpos, -1);
Constantin Kaplinskyf5821e52008-06-23 11:38:51 +0000247 if (is == null) {
248 return null;
249 }
Constantin Kaplinskya9bb64f2008-06-23 08:59:20 +0000250 return new FbsInputStream(is, entry.timestamp, keyData, entry.fbs_skip);
Constantin Kaplinsky1c2865f2008-06-20 18:50:27 +0000251 }
252
Constantin Kaplinsky31b7a242008-06-22 07:24:11 +0000253 private static InputStream openHttpByteRange(URL url, long offset, long len)
Constantin Kaplinskyd2af0822008-06-21 11:41:42 +0000254 throws IOException {
Constantin Kaplinsky31b7a242008-06-22 07:24:11 +0000255 HttpURLConnection conn = (HttpURLConnection)url.openConnection();
256 String rangeSpec = "bytes=" + offset + "-";
257 if (len != -1) {
258 long lastByteOffset = offset + len - 1;
259 rangeSpec += lastByteOffset;
260 }
Constantin Kaplinskyd2af0822008-06-21 11:41:42 +0000261 conn.setRequestProperty("Range", rangeSpec);
262 conn.connect();
Constantin Kaplinsky31b7a242008-06-22 07:24:11 +0000263 InputStream is = conn.getInputStream();
264 if (conn.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
265 System.err.println("HTTP server does not support Range request headers");
266 is.close();
267 return null;
268 }
269 return is;
Constantin Kaplinskyd2af0822008-06-21 11:41:42 +0000270 }
271
Constantin Kaplinsky2859fdc2008-06-19 16:07:52 +0000272}