7 | *
8 | * Unless required by applicable law or agreed to in writing, software distributed under the License is
9 | * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and limitations under the License.
11 | */
12 |
13 | // http://code.google.com/p/wireme/source/browse/src/com/wireme/mediaserver/HttpServer.java?r=b0db9c440b168cd50ca74fb2bec86feea587d4d9
14 |
15 | package com.entertailion.java.caster;
16 |
17 | import java.io.BufferedReader;
18 | import java.io.ByteArrayInputStream;
19 | import java.io.ByteArrayOutputStream;
20 | import java.io.File;
21 | import java.io.FileInputStream;
22 | import java.io.FileOutputStream;
23 | import java.io.IOException;
24 | import java.io.InputStream;
25 | import java.io.InputStreamReader;
26 | import java.io.OutputStream;
27 | import java.io.PrintWriter;
28 | import java.net.ServerSocket;
29 | import java.net.Socket;
30 | import java.net.URLEncoder;
31 | import java.util.Date;
32 | import java.util.Enumeration;
33 | import java.util.Hashtable;
34 | import java.util.Locale;
35 | import java.util.Properties;
36 | import java.util.StringTokenizer;
37 | import java.util.TimeZone;
38 | import java.util.Vector;
39 |
40 | /**
41 | * A simple, tiny, nicely embeddable HTTP 1.0 server in Java Modified from
42 | * NanoHTTPD, you can find it here http://elonen.iki.fi/code/nanohttpd/
43 | */
44 | public class HttpServer {
45 | // ==================================================
46 | // API parts
47 | // ==================================================
48 |
49 | /**
50 | * Override this to customize the server.
51 | *
52 | *
53 | * (By default, this delegates to serveFile() and allows directory listing.)
54 | *
55 | * @param uri
56 | * Percent-decoded URI without parameters, for example
57 | * "/index.cgi"
58 | * @param method
59 | * "GET", "POST" etc.
60 | * @param parms
61 | * Parsed, percent decoded parameters from URI and, in case of
62 | * POST, data.
63 | * @param header
64 | * Header entries, percent decoded
65 | * @return HTTP response, see class Response for details
66 | */
67 | public Response serve(String uri, String method, Properties header, Properties parms, Properties files) {
68 | System.out.println(method + " '" + uri + "' ");
69 |
70 | Enumeration e = header.propertyNames();
71 | while (e.hasMoreElements()) {
72 | String value = (String) e.nextElement();
73 | System.out.println(" HDR: '" + value + "' = '" + header.getProperty(value) + "'");
74 | }
75 | e = parms.propertyNames();
76 | while (e.hasMoreElements()) {
77 | String value = (String) e.nextElement();
78 | System.out.println(" PRM: '" + value + "' = '" + parms.getProperty(value) + "'");
79 | }
80 | e = files.propertyNames();
81 | while (e.hasMoreElements()) {
82 | String value = (String) e.nextElement();
83 | System.out.println(" UPLOADED: '" + value + "' = '" + files.getProperty(value) + "'");
84 | }
85 | return serveFile(uri, header, new File("."), true);
86 | }
87 |
88 | /**
89 | * HTTP response. Return one of these from serve().
90 | */
91 | public static class Response {
92 | /**
93 | * Default constructor: response = HTTP_OK, data = mime = 'null'
94 | */
95 | public Response() {
96 | this.status = HTTP_OK;
97 | }
98 |
99 | /**
100 | * Basic constructor.
101 | */
102 | public Response(String status, String mimeType, InputStream data) {
103 | this.status = status;
104 | this.mimeType = mimeType;
105 | this.data = data;
106 | }
107 |
108 | /**
109 | * Convenience method that makes an InputStream out of given text.
110 | */
111 | public Response(String status, String mimeType, String txt) {
112 | this.status = status;
113 | this.mimeType = mimeType;
114 | try {
115 | this.data = new ByteArrayInputStream(txt.getBytes("UTF-8"));
116 | } catch (java.io.UnsupportedEncodingException uee) {
117 | uee.printStackTrace();
118 | }
119 | }
120 |
121 | /**
122 | * Adds given line to the header.
123 | */
124 | public void addHeader(String name, String value) {
125 | header.put(name, value);
126 | }
127 |
128 | /**
129 | * HTTP status code after processing, e.g. "200 OK", HTTP_OK
130 | */
131 | public String status;
132 |
133 | /**
134 | * MIME type of content, e.g. "text/html"
135 | */
136 | public String mimeType;
137 |
138 | /**
139 | * Data of the response, may be null.
140 | */
141 | public InputStream data;
142 |
143 | /**
144 | * Headers for the HTTP response. Use addHeader() to add lines.
145 | */
146 | public Properties header = new Properties();
147 | }
148 |
149 | /**
150 | * Some HTTP response status codes
151 | */
152 | public static final String HTTP_OK = "200 OK", HTTP_PARTIALCONTENT = "206 Partial Content",
153 | HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable", HTTP_REDIRECT = "301 Moved Permanently", HTTP_FORBIDDEN = "403 Forbidden",
154 | HTTP_NOTFOUND = "404 Not Found", HTTP_BADREQUEST = "400 Bad Request", HTTP_INTERNALERROR = "500 Internal Server Error",
155 | HTTP_NOTIMPLEMENTED = "501 Not Implemented";
156 |
157 | /**
158 | * Common mime types for dynamic content
159 | */
160 | public static final String MIME_PLAINTEXT = "text/plain", MIME_HTML = "text/html", MIME_DEFAULT_BINARY = "application/octet-stream", MIME_XML = "text/xml";
161 |
162 | // ==================================================
163 | // Socket & server code
164 | // ==================================================
165 |
166 | /**
167 | * Starts a HTTP server to given port.
168 | *
169 | * Throws an IOException if the socket is already in use
170 | */
171 | public HttpServer(int port) throws IOException {
172 | myTcpPort = port;
173 | this.myRootDir = new File("/");
174 | myServerSocket = new ServerSocket(myTcpPort);
175 | myThread = new Thread(new Runnable() {
176 | public void run() {
177 | try {
178 | while (true)
179 | new HTTPSession(myServerSocket.accept());
180 | } catch (IOException ioe) {
181 | }
182 | }
183 | });
184 | myThread.setDaemon(true);
185 | myThread.start();
186 | }
187 |
188 | /**
189 | * Stops the server.
190 | */
191 | public void stop() {
192 | try {
193 | myServerSocket.close();
194 | myThread.join();
195 | } catch (Throwable e) {
196 | }
197 | }
198 |
199 | /**
200 | * Handles one session, i.e. parses the HTTP request and returns the
201 | * response.
202 | */
203 | private class HTTPSession implements Runnable {
204 | public HTTPSession(Socket s) {
205 | mySocket = s;
206 | Thread t = new Thread(this);
207 | t.setDaemon(true);
208 | t.start();
209 | }
210 |
211 | public void run() {
212 | try {
213 | InputStream is = mySocket.getInputStream();
214 | if (is == null)
215 | return;
216 |
217 | // Read the first 8192 bytes.
218 | // The full header should fit in here.
219 | // Apache's default header limit is 8KB.
220 | int bufsize = 8192;
221 | byte[] buf = new byte[bufsize];
222 | int rlen = is.read(buf, 0, bufsize);
223 | if (rlen <= 0)
224 | return;
225 |
226 | // Create a BufferedReader for parsing the header.
227 | ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
228 | BufferedReader hin = new BufferedReader(new InputStreamReader(hbis));
229 | Properties pre = new Properties();
230 | Properties parms = new Properties();
231 | Properties header = new Properties();
232 | Properties files = new Properties();
233 |
234 | // Decode the header into parms and header java properties
235 | decodeHeader(hin, pre, parms, header);
236 | String method = pre.getProperty("method");
237 | String uri = pre.getProperty("uri");
238 |
239 | long size = 0x7FFFFFFFFFFFFFFFl;
240 | String contentLength = header.getProperty("content-length");
241 | if (contentLength != null) {
242 | try {
243 | size = Integer.parseInt(contentLength);
244 | } catch (NumberFormatException ex) {
245 | }
246 | }
247 |
248 | // We are looking for the byte separating header from body.
249 | // It must be the last byte of the first two sequential new
250 | // lines.
251 | int splitbyte = 0;
252 | boolean sbfound = false;
253 | while (splitbyte < rlen) {
254 | if (buf[splitbyte] == '\r' && buf[++splitbyte] == '\n' && buf[++splitbyte] == '\r' && buf[++splitbyte] == '\n') {
255 | sbfound = true;
256 | break;
257 | }
258 | splitbyte++;
259 | }
260 | splitbyte++;
261 |
262 | // Write the part of body already read to ByteArrayOutputStream
263 | // f
264 | ByteArrayOutputStream f = new ByteArrayOutputStream();
265 | if (splitbyte < rlen)
266 | f.write(buf, splitbyte, rlen - splitbyte);
267 |
268 | // While Firefox sends on the first read all the data fitting
269 | // our buffer, Chrome and Opera sends only the headers even if
270 | // there is data for the body. So we do some magic here to find
271 | // out whether we have already consumed part of body, if we
272 | // have reached the end of the data to be sent or we should
273 | // expect the first byte of the body at the next read.
274 | if (splitbyte < rlen)
275 | size -= rlen - splitbyte + 1;
276 | else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl)
277 | size = 0;
278 |
279 | // Now read all the body and write it to f
280 | buf = new byte[512];
281 | while (rlen >= 0 && size > 0) {
282 | rlen = is.read(buf, 0, 512);
283 | size -= rlen;
284 | if (rlen > 0)
285 | f.write(buf, 0, rlen);
286 | }
287 |
288 | // Get the raw body as a byte []
289 | byte[] fbuf = f.toByteArray();
290 |
291 | // Create a BufferedReader for easily reading it as string.
292 | ByteArrayInputStream bin = new ByteArrayInputStream(fbuf);
293 | BufferedReader in = new BufferedReader(new InputStreamReader(bin));
294 |
295 | // If the method is POST, there may be parameters
296 | // in data section, too, read it:
297 | if (method.equalsIgnoreCase("POST")) {
298 | String contentType = "";
299 | String contentTypeHeader = header.getProperty("content-type");
300 | StringTokenizer st = new StringTokenizer(contentTypeHeader, "; ");
301 | if (st.hasMoreTokens()) {
302 | contentType = st.nextToken();
303 | }
304 |
305 | if (contentType.equalsIgnoreCase("multipart/form-data")) {
306 | // Handle multipart/form-data
307 | if (!st.hasMoreTokens())
308 | sendError(HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
309 | String boundaryExp = st.nextToken();
310 | st = new StringTokenizer(boundaryExp, "=");
311 | if (st.countTokens() != 2)
312 | sendError(HTTP_BADREQUEST,
313 | "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html");
314 | st.nextToken();
315 | String boundary = st.nextToken();
316 |
317 | decodeMultipartData(boundary, fbuf, in, parms, files);
318 | } else {
319 | // Handle application/x-www-form-urlencoded
320 | String postLine = "";
321 | char pbuf[] = new char[512];
322 | int read = in.read(pbuf);
323 | while (read >= 0 && !postLine.endsWith("\r\n")) {
324 | postLine += String.valueOf(pbuf, 0, read);
325 | read = in.read(pbuf);
326 | }
327 | postLine = postLine.trim();
328 | decodeParms(postLine, parms);
329 | }
330 | }
331 |
332 | // Ok, now do the serve()
333 | Response r = serve(uri, method, header, parms, files);
334 | if (r == null)
335 | sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
336 | else
337 | sendResponse(r.status, r.mimeType, r.header, r.data);
338 |
339 | in.close();
340 | is.close();
341 | } catch (IOException ioe) {
342 | try {
343 | sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
344 | } catch (Throwable t) {
345 | }
346 | } catch (InterruptedException ie) {
347 | // Thrown by sendError, ignore and exit the thread.
348 | } catch (Throwable e) {
349 | }
350 | }
351 |
352 | /**
353 | * Decodes the sent headers and loads the data into java Properties' key
354 | * - value pairs
355 | **/
356 | private void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header) throws InterruptedException {
357 | try {
358 | // Read the request line
359 | String inLine = in.readLine();
360 | if (inLine == null)
361 | return;
362 | StringTokenizer st = new StringTokenizer(inLine);
363 | if (!st.hasMoreTokens())
364 | sendError(HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
365 |
366 | String method = st.nextToken();
367 | pre.put("method", method);
368 |
369 | if (!st.hasMoreTokens())
370 | sendError(HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
371 |
372 | String uri = st.nextToken();
373 |
374 | // Decode parameters from the URI
375 | int qmi = uri.indexOf('?');
376 | if (qmi >= 0) {
377 | decodeParms(uri.substring(qmi + 1), parms);
378 | uri = decodePercent(uri.substring(0, qmi));
379 | } else
380 | uri = decodePercent(uri);
381 |
382 | // If there's another token, it's protocol version,
383 | // followed by HTTP headers. Ignore version but parse headers.
384 | // NOTE: this now forces header names lowercase since they are
385 | // case insensitive and vary by client.
386 | if (st.hasMoreTokens()) {
387 | String line = in.readLine();
388 | while (line != null && line.trim().length() > 0) {
389 | int p = line.indexOf(':');
390 | if (p >= 0)
391 | header.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim());
392 | line = in.readLine();
393 | }
394 | }
395 |
396 | pre.put("uri", uri);
397 | } catch (IOException ioe) {
398 | sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
399 | }
400 | }
401 |
402 | /**
403 | * Decodes the Multipart Body data and put it into java Properties' key
404 | * - value pairs.
405 | **/
406 | private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Properties parms, Properties files) throws InterruptedException {
407 | try {
408 | int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes());
409 | int boundarycount = 1;
410 | String mpline = in.readLine();
411 | while (mpline != null) {
412 | if (mpline.indexOf(boundary) == -1)
413 | sendError(HTTP_BADREQUEST,
414 | "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html");
415 | boundarycount++;
416 | Properties item = new Properties();
417 | mpline = in.readLine();
418 | while (mpline != null && mpline.trim().length() > 0) {
419 | int p = mpline.indexOf(':');
420 | if (p != -1)
421 | item.put(mpline.substring(0, p).trim().toLowerCase(), mpline.substring(p + 1).trim());
422 | mpline = in.readLine();
423 | }
424 | if (mpline != null) {
425 | String contentDisposition = item.getProperty("content-disposition");
426 | if (contentDisposition == null) {
427 | sendError(HTTP_BADREQUEST,
428 | "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html");
429 | }
430 | StringTokenizer st = new StringTokenizer(contentDisposition, "; ");
431 | Properties disposition = new Properties();
432 | while (st.hasMoreTokens()) {
433 | String token = st.nextToken();
434 | int p = token.indexOf('=');
435 | if (p != -1)
436 | disposition.put(token.substring(0, p).trim().toLowerCase(), token.substring(p + 1).trim());
437 | }
438 | String pname = disposition.getProperty("name");
439 | pname = pname.substring(1, pname.length() - 1);
440 |
441 | String value = "";
442 | if (item.getProperty("content-type") == null) {
443 | while (mpline != null && mpline.indexOf(boundary) == -1) {
444 | mpline = in.readLine();
445 | if (mpline != null) {
446 | int d = mpline.indexOf(boundary);
447 | if (d == -1)
448 | value += mpline;
449 | else
450 | value += mpline.substring(0, d - 2);
451 | }
452 | }
453 | } else {
454 | if (boundarycount > bpositions.length)
455 | sendError(HTTP_INTERNALERROR, "Error processing request");
456 | int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]);
457 | String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4);
458 | files.put(pname, path);
459 | value = disposition.getProperty("filename");
460 | value = value.substring(1, value.length() - 1);
461 | do {
462 | mpline = in.readLine();
463 | } while (mpline != null && mpline.indexOf(boundary) == -1);
464 | }
465 | parms.put(pname, value);
466 | }
467 | }
468 | } catch (IOException ioe) {
469 | sendError(HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
470 | }
471 | }
472 |
473 | /**
474 | * Find the byte positions where multipart boundaries start.
475 | **/
476 | public int[] getBoundaryPositions(byte[] b, byte[] boundary) {
477 | int matchcount = 0;
478 | int matchbyte = -1;
479 | Vector matchbytes = new Vector();
480 | for (int i = 0; i < b.length; i++) {
481 | if (b[i] == boundary[matchcount]) {
482 | if (matchcount == 0)
483 | matchbyte = i;
484 | matchcount++;
485 | if (matchcount == boundary.length) {
486 | matchbytes.addElement(Integer.valueOf(matchbyte));
487 | matchcount = 0;
488 | matchbyte = -1;
489 | }
490 | } else {
491 | i -= matchcount;
492 | matchcount = 0;
493 | matchbyte = -1;
494 | }
495 | }
496 | int[] ret = new int[matchbytes.size()];
497 | for (int i = 0; i < ret.length; i++) {
498 | ret[i] = ((Integer) matchbytes.elementAt(i)).intValue();
499 | }
500 | return ret;
501 | }
502 |
503 | /**
504 | * Retrieves the content of a sent file and saves it to a temporary
505 | * file. The full path to the saved file is returned.
506 | **/
507 | private String saveTmpFile(byte[] b, int offset, int len) {
508 | String path = "";
509 | if (len > 0) {
510 | String tmpdir = System.getProperty("java.io.tmpdir");
511 | try {
512 | File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir));
513 | OutputStream fstream = new FileOutputStream(temp);
514 | fstream.write(b, offset, len);
515 | fstream.close();
516 | path = temp.getAbsolutePath();
517 | } catch (Exception e) { // Catch exception if any
518 | System.err.println("Error: " + e.getMessage());
519 | }
520 | }
521 | return path;
522 | }
523 |
524 | /**
525 | * It returns the offset separating multipart file headers from the
526 | * file's data.
527 | **/
528 | private int stripMultipartHeaders(byte[] b, int offset) {
529 | int i = 0;
530 | for (i = offset; i < b.length; i++) {
531 | if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n')
532 | break;
533 | }
534 | return i + 1;
535 | }
536 |
537 | /**
538 | * Decodes the percent encoding scheme.
539 | * For example: "an+example%20string" -> "an example string"
540 | */
541 | private String decodePercent(String str) throws InterruptedException {
542 | try {
543 | StringBuffer sb = new StringBuffer();
544 | for (int i = 0; i < str.length(); i++) {
545 | char c = str.charAt(i);
546 | switch (c) {
547 | case '+':
548 | sb.append(' ');
549 | break;
550 | case '%':
551 | sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16));
552 | i += 2;
553 | break;
554 | default:
555 | sb.append(c);
556 | break;
557 | }
558 | }
559 | return sb.toString();
560 | } catch (Exception e) {
561 | sendError(HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding.");
562 | return null;
563 | }
564 | }
565 |
566 | /**
567 | * Decodes parameters in percent-encoded URI-format ( e.g.
568 | * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
569 | * Properties. NOTE: this doesn't support multiple identical keys due to
570 | * the simplicity of Properties -- if you need multiples, you might want
571 | * to replace the Properties with a Hashtable of Vectors or such.
572 | */
573 | private void decodeParms(String parms, Properties p) throws InterruptedException {
574 | if (parms == null)
575 | return;
576 |
577 | StringTokenizer st = new StringTokenizer(parms, "&");
578 | while (st.hasMoreTokens()) {
579 | String e = st.nextToken();
580 | int sep = e.indexOf('=');
581 | if (sep >= 0) {
582 | p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
583 | }
584 | }
585 | }
586 |
587 | /**
588 | * Returns an error message as a HTTP response and throws
589 | * InterruptedException to stop further request processing.
590 | */
591 | private void sendError(String status, String msg) throws InterruptedException {
592 | sendResponse(status, MIME_PLAINTEXT, null, new ByteArrayInputStream(msg.getBytes()));
593 | throw new InterruptedException();
594 | }
595 |
596 | /**
597 | * Sends given response to the socket.
598 | */
599 | private void sendResponse(String status, String mime, Properties header, InputStream data) {
600 | try {
601 | if (status == null)
602 | throw new Error("sendResponse(): Status can't be null.");
603 |
604 | OutputStream out = mySocket.getOutputStream();
605 | PrintWriter pw = new PrintWriter(out);
606 | pw.print("HTTP/1.0 " + status + " \r\n");
607 |
608 | if (mime != null)
609 | pw.print("Content-Type: " + mime + "\r\n");
610 |
611 | if (header == null || header.getProperty("Date") == null)
612 | pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
613 |
614 | if (header != null) {
615 | Enumeration e = header.keys();
616 | while (e.hasMoreElements()) {
617 | String key = (String) e.nextElement();
618 | String value = header.getProperty(key);
619 | pw.print(key + ": " + value + "\r\n");
620 | }
621 | }
622 |
623 | pw.print("\r\n");
624 | pw.flush();
625 |
626 | if (data != null) {
627 | int pending = data.available(); // This is to support
628 | // partial sends, see
629 | // serveFile()
630 | byte[] buff = new byte[2048];
631 | while (pending > 0) {
632 | int read = data.read(buff, 0, ((pending > 2048) ? 2048 : pending));
633 | if (read <= 0)
634 | break;
635 | out.write(buff, 0, read);
636 | pending -= read;
637 | }
638 | }
639 | out.flush();
640 | out.close();
641 | if (data != null)
642 | data.close();
643 | } catch (IOException ioe) {
644 | // Couldn't write? No can do.
645 | try {
646 | mySocket.close();
647 | } catch (Throwable t) {
648 | }
649 | }
650 | }
651 |
652 | private Socket mySocket;
653 | }
654 |
655 | /**
656 | * URL-encodes everything between "/"-characters. Encodes spaces as '%20'
657 | * instead of '+'.
658 | */
659 | protected String encodeUri(String uri) {
660 | String newUri = "";
661 | StringTokenizer st = new StringTokenizer(uri, "/ ", true);
662 | while (st.hasMoreTokens()) {
663 | String tok = st.nextToken();
664 | if (tok.equals("/"))
665 | newUri += "/";
666 | else if (tok.equals(" "))
667 | newUri += "%20";
668 | else {
669 | newUri += URLEncoder.encode(tok);
670 | // For Java 1.4 you'll want to use this instead:
671 | // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch (
672 | // java.io.UnsupportedEncodingException uee ) {}
673 | }
674 | }
675 | return newUri;
676 | }
677 |
678 | private int myTcpPort;
679 | private final ServerSocket myServerSocket;
680 | private Thread myThread;
681 | private File myRootDir;
682 |
683 | // ==================================================
684 | // File server code
685 | // ==================================================
686 |
687 | /**
688 | * Serves file from homeDir and its' subdirectories (only). Uses only URI,
689 | * ignores all headers and HTTP parameters.
690 | */
691 | public Response serveFile(String uri, Properties header, File homeDir, boolean allowDirectoryListing) {
692 | Response res = null;
693 |
694 | // Make sure we won't die of an exception later
695 | if (!homeDir.isDirectory())
696 | res = new Response(HTTP_INTERNALERROR, MIME_PLAINTEXT, "INTERNAL ERRROR: serveFile(): given homeDir is not a directory.");
697 |
698 | if (res == null) {
699 | // Remove URL arguments
700 | uri = uri.trim().replace(File.separatorChar, '/');
701 | if (uri.indexOf('?') >= 0)
702 | uri = uri.substring(0, uri.indexOf('?'));
703 |
704 | // Prohibit getting out of current directory
705 | if (uri.startsWith("..") || uri.endsWith("..") || uri.indexOf("../") >= 0)
706 | res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Won't serve ../ for security reasons.");
707 | }
708 |
709 | File f = new File(homeDir, uri);
710 | if (res == null && !f.exists())
711 | res = new Response(HTTP_NOTFOUND, MIME_PLAINTEXT, "Error 404, file not found.");
712 |
713 | // List the directory, if necessary
714 | if (res == null && f.isDirectory()) {
715 | // Browsers get confused without '/' after the
716 | // directory, send a redirect.
717 | if (!uri.endsWith("/")) {
718 | uri += "/";
719 | res = new Response(HTTP_REDIRECT, MIME_HTML, "
Redirected: " + uri + "");
720 | res.addHeader("Location", uri);
721 | }
722 |
723 | if (res == null) {
724 | // First try index.html and index.htm
725 | if (new File(f, "index.html").exists())
726 | f = new File(homeDir, uri + "/index.html");
727 | else if (new File(f, "index.htm").exists())
728 | f = new File(homeDir, uri + "/index.htm");
729 | // No index file, list the directory if it is readable
730 | else if (allowDirectoryListing && f.canRead()) {
731 | String[] files = f.list();
732 | String msg = "Directory " + uri + "
";
733 |
734 | if (uri.length() > 1) {
735 | String u = uri.substring(0, uri.length() - 1);
736 | int slash = u.lastIndexOf('/');
737 | if (slash >= 0 && slash < u.length())
738 | msg += "..
";
739 | }
740 |
741 | if (files != null) {
742 | for (int i = 0; i < files.length; ++i) {
743 | File curFile = new File(f, files[i]);
744 | boolean dir = curFile.isDirectory();
745 | if (dir) {
746 | msg += "";
747 | files[i] += "/";
748 | }
749 |
750 | msg += "" + files[i] + "";
751 |
752 | // Show file size
753 | if (curFile.isFile()) {
754 | long len = curFile.length();
755 | msg += " (";
756 | if (len < 1024)
757 | msg += len + " bytes";
758 | else if (len < 1024 * 1024)
759 | msg += len / 1024 + "." + (len % 1024 / 10 % 100) + " KB";
760 | else
761 | msg += len / (1024 * 1024) + "." + len % (1024 * 1024) / 10 % 100 + " MB";
762 |
763 | msg += ")";
764 | }
765 | msg += "
";
766 | if (dir)
767 | msg += "";
768 | }
769 | }
770 | msg += "";
771 | res = new Response(HTTP_OK, MIME_HTML, msg);
772 | } else {
773 | res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: No directory listing.");
774 | }
775 | }
776 | }
777 |
778 | try {
779 | if (res == null) {
780 | // Get MIME type from file name extension, if possible
781 | String mime = null;
782 | int dot = f.getCanonicalPath().lastIndexOf('.');
783 | if (dot >= 0)
784 | mime = (String) theMimeTypes.get(f.getCanonicalPath().substring(dot + 1).toLowerCase());
785 | if (mime == null)
786 | mime = MIME_DEFAULT_BINARY;
787 |
788 | // Support (simple) skipping:
789 | long startFrom = 0;
790 | long endAt = -1;
791 | String range = header.getProperty("range");
792 | if (range != null) {
793 | if (range.startsWith("bytes=")) {
794 | range = range.substring("bytes=".length());
795 | int minus = range.indexOf('-');
796 | try {
797 | if (minus > 0) {
798 | startFrom = Long.parseLong(range.substring(0, minus));
799 | endAt = Long.parseLong(range.substring(minus + 1));
800 | }
801 | } catch (NumberFormatException nfe) {
802 | }
803 | }
804 | }
805 |
806 | // Change return code and add Content-Range header when skipping
807 | // is requested
808 | long fileLen = f.length();
809 | if (range != null && startFrom >= 0) {
810 | if (startFrom >= fileLen) {
811 | res = new Response(HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "");
812 | res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
813 | } else {
814 | if (endAt < 0)
815 | endAt = fileLen - 1;
816 | long newLen = endAt - startFrom + 1;
817 | if (newLen < 0)
818 | newLen = 0;
819 |
820 | final long dataLen = newLen;
821 | FileInputStream fis = new FileInputStream(f) {
822 | public int available() throws IOException {
823 | return (int) dataLen;
824 | }
825 | };
826 | fis.skip(startFrom);
827 |
828 | res = new Response(HTTP_PARTIALCONTENT, mime, fis);
829 | res.addHeader("Content-Length", "" + dataLen);
830 | res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
831 | }
832 | } else {
833 | res = new Response(HTTP_OK, mime, new FileInputStream(f));
834 | res.addHeader("Content-Length", "" + fileLen);
835 | }
836 | }
837 | } catch (IOException ioe) {
838 | res = new Response(HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed.");
839 | }
840 |
841 | res.addHeader("Accept-Ranges", "bytes"); // Announce that the file
842 | // server accepts partial
843 | // content requestes
844 | return res;
845 | }
846 |
847 | /**
848 | * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
849 | */
850 | protected static Hashtable theMimeTypes = new Hashtable();
851 | static {
852 | StringTokenizer st = new StringTokenizer("css text/css "
853 | + "js text/javascript "
854 | + "jet text/javascript "
855 | + // hack to avoid asset compression:
856 | // http://ponystyle.com/blog/2010/03/26/dealing-with-asset-compression-in-android-apps/
857 | "htm text/html " + "html text/html " + "txt text/plain " + "asc text/plain " + "gif image/gif "
858 | + "jpg image/jpeg " + "jpeg image/jpeg " + "png image/png " + "mp3 audio/mpeg " + "m3u audio/mpeg-url "
859 | + "avi video/x-msvideo " + "m4u video/vnd.mpegurl " + "m4v video/x-m4v " + "mov video/quicktime " + "mp4 video/mp4 "
860 | + "mpe video/mpeg " + "mpeg video/mpeg " + "mpg video/mpeg " + "mxu video/vnd.mpegurl " + "qt video/quicktime " + "flv video/x-flv "
861 | + "m3u8 application/x-mpegURL " + "ts video/MP2T " + "3gp video/3gpp " + "wmv video/x-ms-wmv " + "pdf application/pdf "
862 | + "doc application/msword " + "ogg application/x-ogg " + "zip application/octet-stream "
863 | + "exe application/octet-stream " + "class application/octet-stream ");
864 | while (st.hasMoreTokens())
865 | theMimeTypes.put(st.nextToken(), st.nextToken());
866 | }
867 |
868 | /**
869 | * GMT date formatter
870 | */
871 | private static java.text.SimpleDateFormat gmtFrmt;
872 | static {
873 | gmtFrmt = new java.text.SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
874 | gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
875 | }
876 |
877 | /**
878 | * The distribution licence
879 | */
880 | private static final String LICENCE = "Copyright (C) 2001,2005-2011 by Jarno Elonen \n"
881 | + "and Copyright (C) 2010 by Konstantinos Togias \n" + "\n"
882 | + "Redistribution and use in source and binary forms, with or without\n" + "modification, are permitted provided that the following conditions\n"
883 | + "are met:\n" + "\n" + "Redistributions of source code must retain the above copyright notice,\n"
884 | + "this list of conditions and the following disclaimer. Redistributions in\n"
885 | + "binary form must reproduce the above copyright notice, this list of\n"
886 | + "conditions and the following disclaimer in the documentation and/or other\n"
887 | + "materials provided with the distribution. The name of the author may not\n"
888 | + "be used to endorse or promote products derived from this software without\n" + "specific prior written permission. \n" + " \n"
889 | + "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"
890 | + "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"
891 | + "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"
892 | + "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"
893 | + "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"
894 | + "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"
895 | + "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"
896 | + "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"
897 | + "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"
898 | + "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
899 | }
900 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/Log.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.java.caster;
15 |
16 | import java.util.logging.Level;
17 | import java.util.logging.Logger;
18 |
19 | /*
20 | * Map Android style logging to Java style logging
21 | *
22 | * @author leon_nicholls
23 | */
24 | public class Log {
25 | private static Logger Log = Logger.getLogger("anymote");
26 | private static boolean verbose = false;
27 |
28 | public static void setVerbose(boolean enabled) {
29 | verbose = enabled;
30 | }
31 |
32 | public static boolean getVerbose() {
33 | return verbose;
34 | }
35 |
36 | public static void e(String tag, String message, Throwable e) {
37 | if (verbose) {
38 | Log.log(Level.SEVERE, tag + ": " + message, e);
39 | }
40 | }
41 |
42 | public static void e(String tag, String message) {
43 | if (verbose) {
44 | Log.log(Level.SEVERE, tag + ": " + message);
45 | }
46 | }
47 |
48 | public static void i(String tag, String message, Throwable e) {
49 | if (verbose) {
50 | Log.log(Level.INFO, tag + ": " + message, e);
51 | }
52 | }
53 |
54 | public static void i(String tag, String message) {
55 | if (verbose) {
56 | Log.log(Level.INFO, tag + ": " + message);
57 | }
58 | }
59 |
60 | public static void d(String tag, String message, Throwable e) {
61 | if (verbose) {
62 | Log.log(Level.CONFIG, tag + ": " + message, e);
63 | }
64 | }
65 |
66 | public static void d(String tag, String message) {
67 | if (verbose) {
68 | Log.log(Level.CONFIG, tag + ": " + message);
69 | System.out.println(tag + ": " + message); // TODO
70 | }
71 | }
72 |
73 | public static void v(String tag, String message, Throwable e) {
74 | if (verbose) {
75 | Log.log(Level.FINEST, tag + ": " + message, e);
76 | }
77 | }
78 |
79 | public static void v(String tag, String message) {
80 | if (verbose) {
81 | Log.log(Level.FINEST, tag + ": " + message);
82 | }
83 | }
84 |
85 | public static void w(String tag, String message, Throwable e) {
86 | if (verbose) {
87 | Log.log(Level.WARNING, tag + ": " + message, e);
88 | }
89 | }
90 |
91 | public static void w(String tag, String message) {
92 | if (verbose) {
93 | Log.log(Level.WARNING, tag + ": " + message);
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/Main.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.java.caster;
15 |
16 | import java.io.PrintWriter;
17 | import java.io.StringWriter;
18 | import java.net.InetAddress;
19 | import java.util.HashMap;
20 | import java.util.Properties;
21 |
22 | import org.apache.commons.cli.CommandLine;
23 | import org.apache.commons.cli.CommandLineParser;
24 | import org.apache.commons.cli.HelpFormatter;
25 | import org.apache.commons.cli.Option;
26 | import org.apache.commons.cli.OptionBuilder;
27 | import org.apache.commons.cli.Options;
28 | import org.apache.commons.cli.ParseException;
29 | import org.apache.commons.cli.PosixParser;
30 | import org.json.simple.JSONArray;
31 | import org.json.simple.JSONObject;
32 |
33 | import com.entertailion.java.caster.HttpServer.Response;
34 |
35 | /**
36 | * Command line ChromeCast client: java -jar caster.jar -h
37 | * https://github.com/entertailion/Caster
38 | *
39 | * @author leon_nicholls
40 | *
41 | */
42 | public class Main {
43 |
44 | private static final String LOG_TAG = "Main";
45 |
46 | // TODO Add your own app id here
47 | private static final String APP_ID = "YOUR_APP_ID";
48 |
49 | public static final String VERSION = "0.4";
50 |
51 | private static Platform platform = new Platform();
52 | private static String appId = APP_ID;
53 |
54 | /**
55 | * @param args
56 | */
57 | public static void main(String[] args) {
58 | // http://commons.apache.org/proper/commons-cli/usage.html
59 | Option help = new Option("h", "help", false, "Print this help message");
60 | Option version = new Option("V", "version", false, "Print version information");
61 | Option list = new Option("l", "list", false, "List ChromeCast devices");
62 | Option verbose = new Option("v", "verbose", false, "Verbose debug logging");
63 | Option transcode = new Option("t", "transcode", false, "Transcode media; -f also required");
64 | Option rest = new Option("r", "rest", false, "REST API server");
65 |
66 | Option url = OptionBuilder.withLongOpt("stream").hasArg().withValueSeparator().withDescription("HTTP URL for streaming content; -d also required")
67 | .create("s");
68 |
69 | Option server = OptionBuilder.withLongOpt("device").hasArg().withValueSeparator().withDescription("ChromeCast device IP address").create("d");
70 |
71 | Option id = OptionBuilder.withLongOpt("app-id").hasArg().withValueSeparator().withDescription("App ID for whitelisted device").create("id");
72 |
73 | Option mediaFile = OptionBuilder.withLongOpt("file").hasArg().withValueSeparator().withDescription("Local media file; -d also required").create("f");
74 |
75 | Option transcodingParameters = OptionBuilder.withLongOpt("transcode-parameters").hasArg().withValueSeparator()
76 | .withDescription("Transcode parameters; -t also required").create("tp");
77 |
78 | Option restPort = OptionBuilder.withLongOpt("rest-port").hasArg().withValueSeparator().withDescription("REST API port; default 8080")
79 | .create("rp");
80 |
81 | Options options = new Options();
82 | options.addOption(help);
83 | options.addOption(version);
84 | options.addOption(list);
85 | options.addOption(verbose);
86 | options.addOption(url);
87 | options.addOption(server);
88 | options.addOption(id);
89 | options.addOption(mediaFile);
90 | options.addOption(transcode);
91 | options.addOption(transcodingParameters);
92 | options.addOption(rest);
93 | options.addOption(restPort);
94 | // create the command line parser
95 | CommandLineParser parser = new PosixParser();
96 |
97 | //String[] arguments = new String[] { "-vr" };
98 |
99 | try {
100 | // parse the command line arguments
101 | CommandLine line = parser.parse(options, args);
102 | Option[] lineOptions = line.getOptions();
103 | if (lineOptions.length == 0) {
104 | System.out.println("caster: try 'java -jar caster.jar -h' for more information");
105 | System.exit(0);
106 | }
107 |
108 | Log.setVerbose(line.hasOption("v"));
109 |
110 | // Custom app-id
111 | if (line.hasOption("id")) {
112 | Log.d(LOG_TAG, line.getOptionValue("id"));
113 | appId = line.getOptionValue("id");
114 | }
115 |
116 | // Print version
117 | if (line.hasOption("V")) {
118 | System.out.println("Caster version " + VERSION);
119 | }
120 |
121 | // List ChromeCast devices
122 | if (line.hasOption("l")) {
123 | final DeviceFinder deviceFinder = new DeviceFinder(new DeviceFinderListener() {
124 |
125 | @Override
126 | public void discoveringDevices(DeviceFinder deviceFinder) {
127 | Log.d(LOG_TAG, "discoveringDevices");
128 | }
129 |
130 | @Override
131 | public void discoveredDevices(DeviceFinder deviceFinder) {
132 | Log.d(LOG_TAG, "discoveredDevices");
133 | TrackedDialServers trackedDialServers = deviceFinder.getTrackedDialServers();
134 | for (DialServer dialServer : trackedDialServers) {
135 | System.out.println(dialServer.toString()); // keep system for output
136 | }
137 | }
138 |
139 | });
140 | deviceFinder.discoverDevices();
141 | }
142 |
143 | // Stream media from internet
144 | if (line.hasOption("s") && line.hasOption("d")) {
145 | Log.d(LOG_TAG, line.getOptionValue("d"));
146 | Log.d(LOG_TAG, line.getOptionValue("s"));
147 | try {
148 | Playback playback = new Playback(platform, appId, new DialServer(InetAddress.getByName(line.getOptionValue("d"))), new PlaybackListener() {
149 | private int time;
150 | private int duration;
151 | private int state;
152 |
153 | @Override
154 | public void updateTime(Playback playback, int time) {
155 | Log.d(LOG_TAG, "updateTime: " + time);
156 | this.time = time;
157 | }
158 |
159 | @Override
160 | public void updateDuration(Playback playback, int duration) {
161 | Log.d(LOG_TAG, "updateDuration: " + duration);
162 | this.duration = duration;
163 | }
164 |
165 | @Override
166 | public void updateState(Playback playback, int state) {
167 | Log.d(LOG_TAG, "updateState: " + state);
168 | // Stop the app if the video reaches the end
169 | if (time > 0 && time == duration && state == 0) {
170 | playback.doStop();
171 | System.exit(0);
172 | }
173 | }
174 |
175 | public int getTime() {
176 | return time;
177 | }
178 |
179 | public int getDuration() {
180 | return duration;
181 | }
182 |
183 | public int getState() {
184 | return state;
185 | }
186 |
187 | });
188 | playback.stream(line.getOptionValue("s"));
189 | } catch (Exception e) {
190 | e.printStackTrace();
191 | System.exit(1);
192 | }
193 | }
194 |
195 | // Play local media file
196 | if (line.hasOption("f") && line.hasOption("d")) {
197 | Log.d(LOG_TAG, line.getOptionValue("d"));
198 | Log.d(LOG_TAG, line.getOptionValue("f"));
199 |
200 | final String file = line.getOptionValue("f");
201 | String device = line.getOptionValue("d");
202 |
203 | try {
204 | Playback playback = new Playback(platform, appId, new DialServer(InetAddress.getByName(device)), new PlaybackListener() {
205 | private int time;
206 | private int duration;
207 | private int state;
208 |
209 | @Override
210 | public void updateTime(Playback playback, int time) {
211 | Log.d(LOG_TAG, "updateTime: " + time);
212 | this.time = time;
213 | }
214 |
215 | @Override
216 | public void updateDuration(Playback playback, int duration) {
217 | Log.d(LOG_TAG, "updateDuration: " + duration);
218 | this.duration = duration;
219 | }
220 |
221 | @Override
222 | public void updateState(Playback playback, int state) {
223 | Log.d(LOG_TAG, "updateState: " + state);
224 | // Stop the app if the video reaches the end
225 | if (time > 0 && time == duration && state == 0) {
226 | playback.doStop();
227 | System.exit(0);
228 | }
229 | }
230 |
231 | public int getTime() {
232 | return time;
233 | }
234 |
235 | public int getDuration() {
236 | return duration;
237 | }
238 |
239 | public int getState() {
240 | return state;
241 | }
242 |
243 | });
244 | if (line.hasOption("t") && line.hasOption("tp")) {
245 | playback.setTranscodingParameters(line.getOptionValue("tp"));
246 | }
247 | playback.play(file, line.hasOption("t"));
248 | } catch (Exception e) {
249 | e.printStackTrace();
250 | System.exit(1);
251 | }
252 | }
253 |
254 | // REST API server
255 | if (line.hasOption("r")) {
256 | final DeviceFinder deviceFinder = new DeviceFinder(new DeviceFinderListener() {
257 |
258 | @Override
259 | public void discoveringDevices(DeviceFinder deviceFinder) {
260 | Log.d(LOG_TAG, "discoveringDevices");
261 | }
262 |
263 | @Override
264 | public void discoveredDevices(DeviceFinder deviceFinder) {
265 | Log.d(LOG_TAG, "discoveredDevices");
266 | TrackedDialServers trackedDialServers = deviceFinder.getTrackedDialServers();
267 | for (DialServer dialServer : trackedDialServers) {
268 | Log.d(LOG_TAG, dialServer.toString());
269 | }
270 | }
271 |
272 | });
273 | deviceFinder.discoverDevices();
274 |
275 | int port = 0;
276 | if (line.hasOption("rp")) {
277 | try {
278 | port = Integer.parseInt(line.getOptionValue("rp"));
279 | } catch (NumberFormatException e) {
280 | Log.e(LOG_TAG, "invalid rest port", e);
281 | }
282 | }
283 |
284 | Playback.startWebserver(port, new WebListener() {
285 | String[] prefixes = { "/playback", "/devices" };
286 | HashMap playbackMap = new HashMap();
287 | HashMap playbackListenerMap = new HashMap();
288 |
289 | final class RestPlaybackListener implements PlaybackListener {
290 | private String device;
291 | private int time;
292 | private int duration;
293 | private int state;
294 |
295 | public RestPlaybackListener(String device) {
296 | this.device = device;
297 | }
298 |
299 | @Override
300 | public void updateTime(Playback playback, int time) {
301 | Log.d(LOG_TAG, "updateTime: " + time);
302 | this.time = time;
303 | }
304 |
305 | @Override
306 | public void updateDuration(Playback playback, int duration) {
307 | Log.d(LOG_TAG, "updateDuration: " + duration);
308 | this.duration = duration;
309 | }
310 |
311 | @Override
312 | public void updateState(Playback playback, int state) {
313 | Log.d(LOG_TAG, "updateState: " + state);
314 | this.state = state;
315 | // Stop the app if the video reaches the end
316 | if (this.time > 0 && this.time == this.duration && state == 0) {
317 | playback.doStop();
318 | playbackMap.remove(device);
319 | playbackListenerMap.remove(device);
320 | }
321 | }
322 |
323 | public int getTime() {
324 | return time;
325 | }
326 |
327 | public int getDuration() {
328 | return duration;
329 | }
330 |
331 | public int getState() {
332 | return state;
333 | }
334 |
335 | }
336 |
337 | @Override
338 | public Response handleRequest(String uri, String method, Properties header, Properties parms) {
339 | Log.d(LOG_TAG, "handleRequest: " + uri);
340 |
341 | if (method.equals("GET")) {
342 | if (uri.startsWith(prefixes[0])) { // playback
343 | String device = parms.getProperty("device");
344 | if (device != null) {
345 | RestPlaybackListener playbackListener = playbackListenerMap.get(device);
346 | if (playbackListener != null) {
347 | // https://code.google.com/p/json-simple/wiki/EncodingExamples
348 | JSONObject obj = new JSONObject();
349 | obj.put("time", playbackListener.getTime());
350 | obj.put("duration", playbackListener.getDuration());
351 | switch (playbackListener.getState()) {
352 | case 0:
353 | obj.put("state", "idle");
354 | break;
355 | case 1:
356 | obj.put("state", "stopped");
357 | break;
358 | case 2:
359 | obj.put("state", "playing");
360 | break;
361 | default:
362 | obj.put("state", "idle");
363 | break;
364 | }
365 | return new Response(HttpServer.HTTP_OK, "text/plain", obj.toJSONString());
366 | } else {
367 | // Nothing is playing
368 | JSONObject obj = new JSONObject();
369 | obj.put("time", 0);
370 | obj.put("duration", 0);
371 | obj.put("state", "stopped");
372 | return new Response(HttpServer.HTTP_OK, "text/plain", obj.toJSONString());
373 | }
374 | }
375 | } else if (uri.startsWith(prefixes[1])) { // devices
376 | // https://code.google.com/p/json-simple/wiki/EncodingExamples
377 | JSONArray list = new JSONArray();
378 | TrackedDialServers trackedDialServers = deviceFinder.getTrackedDialServers();
379 | for (DialServer dialServer : trackedDialServers) {
380 | JSONObject obj = new JSONObject();
381 | obj.put("name", dialServer.getFriendlyName());
382 | obj.put("ip_address", dialServer.getIpAddress().getHostAddress());
383 | list.add(obj);
384 | }
385 | return new Response(HttpServer.HTTP_OK, "text/plain", list.toJSONString());
386 | }
387 | } else if (method.equals("POST")) {
388 | if (uri.startsWith(prefixes[0])) { // playback
389 | String device = parms.getProperty("device");
390 | if (device != null) {
391 | String stream = parms.getProperty("stream");
392 | String file = parms.getProperty("file");
393 | String state = parms.getProperty("state");
394 | String transcode = parms.getProperty("transcode");
395 | String transcodeParameters = parms.getProperty("transcode-parameters");
396 | Log.d(LOG_TAG, "transcodeParameters="+transcodeParameters);
397 | if (stream != null) {
398 | try {
399 | if (playbackMap.get(device) == null) {
400 | DialServer dialServer = deviceFinder.getTrackedDialServers().findDialServer(InetAddress.getByName(device));
401 | if (dialServer != null) {
402 | RestPlaybackListener playbackListener = new RestPlaybackListener(device);
403 | playbackMap.put(device, new Playback(platform, appId, dialServer, playbackListener));
404 | playbackListenerMap.put(device, playbackListener);
405 | }
406 | }
407 | Playback playback = playbackMap.get(device);
408 | if (playback != null) {
409 | playback.stream(stream);
410 | return new Response(HttpServer.HTTP_OK, "text/plain", "Ok");
411 | }
412 | } catch (Exception e1) {
413 | Log.e(LOG_TAG, "playback", e1);
414 | }
415 | } else if (file != null) {
416 | try {
417 | if (playbackMap.get(device) == null) {
418 | DialServer dialServer = deviceFinder.getTrackedDialServers().findDialServer(InetAddress.getByName(device));
419 | if (dialServer != null) {
420 | RestPlaybackListener playbackListener = new RestPlaybackListener(device);
421 | playbackMap.put(device, new Playback(platform, appId, dialServer, playbackListener));
422 | playbackListenerMap.put(device, playbackListener);
423 | }
424 | }
425 | Playback playback = playbackMap.get(device);
426 | if (transcodeParameters!=null) {
427 | playback.setTranscodingParameters(transcodeParameters);
428 | }
429 | if (playback != null) {
430 | playback.play(file, transcode!=null);
431 | return new Response(HttpServer.HTTP_OK, "text/plain", "Ok");
432 | }
433 | } catch (Exception e1) {
434 | Log.e(LOG_TAG, "playback", e1);
435 | }
436 | } else if (state != null) {
437 | try {
438 | if (playbackMap.get(device) == null) {
439 | DialServer dialServer = deviceFinder.getTrackedDialServers().findDialServer(InetAddress.getByName(device));
440 | if (dialServer != null) {
441 | RestPlaybackListener playbackListener = new RestPlaybackListener(device);
442 | playbackMap.put(device, new Playback(platform, appId, dialServer, playbackListener));
443 | playbackListenerMap.put(device, playbackListener);
444 | }
445 | }
446 | Playback playback = playbackMap.get(device);
447 | if (playback != null) {
448 | // Handle case where current app wasn't started with caster
449 | playback.setDialServer(deviceFinder.getTrackedDialServers().findDialServer(InetAddress.getByName(device)));
450 | // Change the playback state
451 | if (state.equals("play")) {
452 | playback.doPlay();
453 | return new Response(HttpServer.HTTP_OK, "text/plain", "Ok");
454 | } else if (state.equals("pause")) {
455 | playback.doPause();
456 | return new Response(HttpServer.HTTP_OK, "text/plain", "Ok");
457 | } else if (state.equals("stop")) {
458 | playback.doStop();
459 | playbackMap.remove(device);
460 | playbackListenerMap.remove(device);
461 | return new Response(HttpServer.HTTP_OK, "text/plain", "Ok");
462 | } else {
463 | Log.e(LOG_TAG, "playback invalid state: "+state);
464 | }
465 | }
466 | } catch (Exception e1) {
467 | Log.e(LOG_TAG, "playback", e1);
468 | }
469 | }
470 | }
471 | }
472 | }
473 |
474 | return new Response(HttpServer.HTTP_BADREQUEST, "text/plain", "Bad Request");
475 | }
476 |
477 | @Override
478 | public String[] uriPrefixes() {
479 | return prefixes;
480 | }
481 |
482 | });
483 | Log.d(LOG_TAG, "REST server ready");
484 |
485 | // Run forever...
486 | while (true) {
487 | try {
488 | Thread.sleep(1000);
489 | } catch (InterruptedException e) {
490 | }
491 | }
492 | }
493 |
494 | // Print help
495 | if (line.hasOption("h")) {
496 | printHelp(options);
497 | }
498 | } catch (ParseException exp) {
499 | System.out.println("ERROR: " + exp.getMessage());
500 | System.out.println();
501 | printHelp(options);
502 | }
503 | }
504 |
505 | private static void printHelp(Options options) {
506 | StringWriter out = new StringWriter();
507 | HelpFormatter formatter = new HelpFormatter();
508 | formatter.printHelp(new PrintWriter(out), 80, "java -jar caster.jar", "\n", options, 2, 2, "", true);
509 | System.out.println(out.toString());
510 | }
511 |
512 | }
513 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/Platform.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.java.caster;
15 |
16 | import java.io.FileInputStream;
17 | import java.io.FileNotFoundException;
18 | import java.io.FileOutputStream;
19 | import java.net.Inet4Address;
20 | import java.net.InetAddress;
21 | import java.net.InterfaceAddress;
22 | import java.net.NetworkInterface;
23 | import java.util.Enumeration;
24 | import java.util.Iterator;
25 |
26 | /**
27 | * Platform-specific capabilities
28 | */
29 | public class Platform {
30 | private static final String LOG_TAG = "Platform";
31 |
32 | public static final int NAME = 0;
33 | public static final int CERTIFICATE_NAME = 1;
34 | public static final int UNIQUE_ID = 2;
35 | public static final int NETWORK_NAME = 3;
36 | public static final int MODE_PRIVATE = 0;
37 |
38 | /**
39 | * Open a file for output
40 | *
41 | * @param name
42 | * @param mode
43 | * @return
44 | * @throws FileNotFoundException
45 | */
46 | public FileOutputStream openFileOutput(String name, int mode) throws FileNotFoundException {
47 | // TODO support mode parameter
48 | return new FileOutputStream(name);
49 | }
50 |
51 | /**
52 | * Open a file for input
53 | *
54 | * @param name
55 | * @return
56 | * @throws FileNotFoundException
57 | */
58 | public FileInputStream openFileInput(String name) throws FileNotFoundException {
59 | return new FileInputStream(name);
60 | }
61 |
62 | private static InterfaceAddress getPreferredInetAddress(String prefix) {
63 | InterfaceAddress selectedInterfaceAddress = null;
64 | try {
65 | Enumeration list = NetworkInterface.getNetworkInterfaces();
66 |
67 | while (list.hasMoreElements()) {
68 | NetworkInterface iface = list.nextElement();
69 | if (iface == null)
70 | continue;
71 | Log.d(LOG_TAG, "interface=" + iface.getName());
72 | Iterator it = iface.getInterfaceAddresses().iterator();
73 | while (it.hasNext()) {
74 | InterfaceAddress interfaceAddress = it.next();
75 | if (interfaceAddress == null)
76 | continue;
77 | InetAddress address = interfaceAddress.getAddress();
78 | Log.d(LOG_TAG, "address=" + address);
79 | if (address instanceof Inet4Address) {
80 | // Only pick an interface that is likely to be on the
81 | // same subnet as the selected ChromeCast device
82 | if (address.getHostAddress().toString().startsWith(prefix)) {
83 | return interfaceAddress;
84 | }
85 | }
86 | }
87 | }
88 | } catch (Exception ex) {
89 | }
90 | return selectedInterfaceAddress;
91 | }
92 |
93 | /**
94 | * Get the network address.
95 | *
96 | * @return
97 | */
98 | public Inet4Address getNetworAddress(String dialServerAddress) {
99 | Inet4Address selectedInetAddress = null;
100 | try {
101 | InterfaceAddress interfaceAddress = null;
102 | if (dialServerAddress != null) {
103 | String prefix = dialServerAddress.substring(0, dialServerAddress.indexOf('.') + 1);
104 | Log.d(LOG_TAG, "prefix=" + prefix);
105 | interfaceAddress = getPreferredInetAddress(prefix);
106 | } else {
107 | InterfaceAddress oneNineTwoInetAddress = getPreferredInetAddress("192.");
108 | if (oneNineTwoInetAddress != null) {
109 | interfaceAddress = oneNineTwoInetAddress;
110 | } else {
111 | InterfaceAddress oneSevenTwoInetAddress = getPreferredInetAddress("172.");
112 | if (oneSevenTwoInetAddress != null) {
113 | interfaceAddress = oneSevenTwoInetAddress;
114 | } else {
115 | interfaceAddress = getPreferredInetAddress("10.");
116 | }
117 | }
118 | }
119 | if (interfaceAddress != null) {
120 | InetAddress networkAddress = interfaceAddress.getAddress();
121 | Log.d(LOG_TAG, "networkAddress=" + networkAddress);
122 | if (networkAddress != null) {
123 | return (Inet4Address) networkAddress;
124 | }
125 | }
126 | } catch (Exception ex) {
127 | }
128 |
129 | return selectedInetAddress;
130 | }
131 |
132 | /**
133 | * Get the platform version code
134 | *
135 | * @return versionCode
136 | */
137 | public int getVersionCode() {
138 | return 1;
139 | }
140 |
141 | /**
142 | * Get platform strings
143 | *
144 | * @param id
145 | * @return
146 | */
147 | public String getString(int id) {
148 | switch (id) {
149 | case NAME:
150 | return "Raspberry PI";
151 | case CERTIFICATE_NAME:
152 | return "java";
153 | case UNIQUE_ID:
154 | return "emulator";
155 | case NETWORK_NAME:
156 | return "wired"; // (Wifi would be SSID)
157 | default:
158 | return null;
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/Playback.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.java.caster;
15 |
16 | import java.io.File;
17 | import java.io.IOException;
18 | import java.net.Inet4Address;
19 | import java.net.ServerSocket;
20 | import java.util.Properties;
21 |
22 | import uk.co.caprica.vlcj.player.MediaPlayer;
23 | import uk.co.caprica.vlcj.player.MediaPlayerEventAdapter;
24 | import uk.co.caprica.vlcj.player.MediaPlayerFactory;
25 | import uk.co.caprica.vlcj.runtime.RuntimeUtil;
26 |
27 | import com.sun.jna.Native;
28 | import com.sun.jna.NativeLibrary;
29 |
30 | /**
31 | * @author leon_nicholls
32 | *
33 | */
34 | public class Playback {
35 |
36 | private static final String LOG_TAG = "Playback";
37 |
38 | private static final String VLC_MAC = "/Applications/VLC.app/Contents/MacOS/lib";
39 | private static final String VLC_WINDOWS1 = "C:\\Program Files\\VideoLAN\\VLC";
40 | private static final String VLC_WINDOWS2 = "C:\\Program Files (x86)\\VideoLAN\\VLC";
41 |
42 | public static final String TRANSCODING_PARAMETERS = "vcodec=VP80,vb=1000,vfilter=canvas{width=640,height=360},acodec=vorb,ab=128,channels=2,samplerate=44100,threads=2";
43 |
44 | private static EmbeddedServer embeddedServer;
45 | private static int port = EmbeddedServer.HTTP_PORT;
46 |
47 | private static MediaPlayerFactory mediaPlayerFactory;
48 | private static MediaPlayer mediaPlayer;
49 | private static String transcodingParameterValues = TRANSCODING_PARAMETERS;
50 |
51 | private PlaybackListener playbackListener;
52 | private RampClient rampClient;
53 | private String appId;
54 | private DialServer dialServer;
55 | private Platform platform;
56 | private boolean isTranscoding;
57 |
58 | public Playback(Platform platform, String appId, DialServer dialServer, PlaybackListener playbackListener) {
59 | this.platform = platform;
60 | this.appId = appId;
61 | this.dialServer = dialServer;
62 | this.playbackListener = playbackListener;
63 | this.rampClient = new RampClient(this, playbackListener);
64 | }
65 |
66 | public void stream(final String u) {
67 | Log.d(LOG_TAG, "stream: " + rampClient);
68 | if (!rampClient.isClosed()) {
69 | rampClient.closeCurrentApp(dialServer);
70 | }
71 | if (dialServer != null) {
72 | rampClient.launchApp(appId, dialServer, null);
73 | // wait for socket to be ready...
74 | new Thread(new Runnable() {
75 | public void run() {
76 | while (!rampClient.isStarted() && !rampClient.isClosed()) {
77 | try {
78 | // make less than 3 second ping time
79 | Thread.sleep(500);
80 | } catch (InterruptedException e) {
81 | }
82 | }
83 | if (!rampClient.isClosed()) {
84 | try {
85 | Thread.sleep(500);
86 | } catch (InterruptedException e) {
87 | }
88 | rampClient.load(u);
89 | }
90 | }
91 | }).start();
92 | } else {
93 | Log.d(LOG_TAG, "stream: dialserver null");
94 | }
95 | }
96 |
97 | public void launch(final String body) {
98 | Log.d(LOG_TAG, "launch: " + rampClient);
99 | if (!rampClient.isClosed()) {
100 | rampClient.closeCurrentApp(dialServer);
101 | }
102 | if (dialServer != null) {
103 | rampClient.launchApp(appId, dialServer, body);
104 | } else {
105 | Log.d(LOG_TAG, "launch: dialserver null");
106 | }
107 | }
108 |
109 | public void setTranscodingParameters(String transcodingParameterValues) {
110 | this.transcodingParameterValues = transcodingParameterValues;
111 | }
112 |
113 | public void play(final String file, final boolean isTranscoding) {
114 | Log.d(LOG_TAG, "play: " + rampClient);
115 | this.isTranscoding = isTranscoding;
116 | if (isTranscoding) {
117 | initializeTranscoder();
118 |
119 | mediaPlayerFactory = new MediaPlayerFactory();
120 | mediaPlayer = mediaPlayerFactory.newHeadlessMediaPlayer();
121 | // Add a component to be notified of player events
122 | mediaPlayer.addMediaPlayerEventListener(new MediaPlayerEventAdapter() {
123 | public void opening(MediaPlayer mediaPlayer) {
124 | Log.d(LOG_TAG, "VLC Transcoding: Opening");
125 | }
126 |
127 | public void buffering(MediaPlayer mediaPlayer, float newCache) {
128 | Log.d(LOG_TAG, "VLC Transcoding: Buffering");
129 | }
130 |
131 | public void playing(MediaPlayer mediaPlayer) {
132 | Log.d(LOG_TAG, "VLC Transcoding: Playing");
133 | if (playbackListener != null) {
134 | playbackListener.updateDuration(Playback.this, (int) (mediaPlayer.getLength() / 1000.0f));
135 | }
136 | }
137 |
138 | public void paused(MediaPlayer mediaPlayer) {
139 | Log.d(LOG_TAG, "VLC Transcoding: Paused");
140 | }
141 |
142 | public void stopped(MediaPlayer mediaPlayer) {
143 | Log.d(LOG_TAG, "VLC Transcoding: Stopped");
144 | }
145 |
146 | public void finished(MediaPlayer mediaPlayer) {
147 | Log.d(LOG_TAG, "VLC Transcoding: Finished");
148 | }
149 |
150 | public void error(MediaPlayer mediaPlayer) {
151 | Log.d(LOG_TAG, "VLC Transcoding: Error");
152 | }
153 |
154 | public void videoOutput(MediaPlayer mediaPlayer, int newCount) {
155 | Log.d(LOG_TAG, "VLC Transcoding: VideoOutput");
156 | }
157 | });
158 |
159 | // Find a port for VLC HTTP server
160 | boolean started = false;
161 | int vlcPort = port + 1;
162 | while (!started) {
163 | try {
164 | ServerSocket serverSocket = new ServerSocket(vlcPort);
165 | Log.d(LOG_TAG, "Available port for VLC: " + vlcPort);
166 | started = true;
167 | serverSocket.close();
168 | } catch (IOException ioe) {
169 | vlcPort++;
170 | } catch (Exception ex) {
171 | break;
172 | }
173 | }
174 | port = vlcPort;
175 | }
176 |
177 | Properties systemProperties = System.getProperties();
178 | systemProperties.setProperty(EmbeddedServer.CURRENT_FILE, file); // EmbeddedServer.serveFile
179 |
180 | int pos = file.lastIndexOf('.');
181 | String extension = "";
182 | if (pos > -1) {
183 | extension = file.substring(pos);
184 | }
185 | if (dialServer != null) {
186 | Inet4Address address = platform.getNetworAddress(dialServer.getIpAddress().getHostAddress());
187 | if (address != null) {
188 | String mediaUrl = null;
189 | if (isTranscoding) {
190 | // http://192.168.0.8:8087/cast.webm
191 | mediaUrl = "http://" + address.getHostAddress() + ":" + port + "/cast.webm";
192 | } else {
193 | startWebserver(null);
194 |
195 | mediaUrl = "http://" + address.getHostAddress() + ":" + port + "/video" + extension;
196 | }
197 | Log.d(LOG_TAG, "mediaUrl=" + mediaUrl);
198 | if (!rampClient.isClosed()) {
199 | rampClient.closeCurrentApp(dialServer);
200 | }
201 | rampClient.launchApp(appId, dialServer, null);
202 | final String playbackUrl = mediaUrl;
203 | // wait for socket to be ready...
204 | new Thread(new Runnable() {
205 | public void run() {
206 | while (!rampClient.isStarted() && !rampClient.isClosed()) {
207 | try {
208 | // make less than 3 second ping time
209 | Thread.sleep(500);
210 | } catch (InterruptedException e) {
211 | }
212 | }
213 | if (!rampClient.isClosed()) {
214 | try {
215 | Thread.sleep(500);
216 | } catch (InterruptedException e) {
217 | }
218 | if (!rampClient.isClosed()) {
219 | if (isTranscoding) {
220 | final String transcodingOptions[] = {
221 | ":sout=#transcode{" + transcodingParameterValues + "}:http{mux=webm,dst=:" + port + "/cast.webm}", ":sout-keep" };
222 | mediaPlayer.playMedia(file, transcodingOptions);
223 | }
224 | rampClient.load(playbackUrl);
225 | }
226 | }
227 | }
228 | }).start();
229 | } else {
230 | Log.d(LOG_TAG, "could not find a network interface");
231 | }
232 | } else {
233 | Log.d(LOG_TAG, "play: dialserver null");
234 | }
235 | }
236 |
237 | /**
238 | * Start a web server to serve the videos to the media player on the
239 | * ChromeCast device
240 | */
241 | public static void startWebserver(WebListener weblistener) {
242 | startWebserver(EmbeddedServer.HTTP_PORT, weblistener);
243 | }
244 |
245 | public static void startWebserver(int customPort, WebListener weblistener) {
246 | if (customPort > 0) {
247 | port = customPort;
248 | }
249 | boolean started = false;
250 | while (!started) {
251 | try {
252 | embeddedServer = new EmbeddedServer(port);
253 | Log.d(LOG_TAG, "Started web server on port " + port);
254 | started = true;
255 | embeddedServer.setWebListener(weblistener);
256 | } catch (IOException ioe) {
257 | // ioe.printStackTrace();
258 | port++;
259 | } catch (Exception ex) {
260 | break;
261 | }
262 | }
263 | }
264 |
265 | private static void initializeTranscoder() {
266 | // VLC wrapper for Java:
267 | // http://www.capricasoftware.co.uk/projects/vlcj/index.html
268 | if (Log.getVerbose()) {
269 | System.setProperty("vlcj.log", "DEBUG");
270 | } else {
271 | // System.setProperty("vlcj.log", "INFO");
272 | }
273 | try {
274 | Log.d(LOG_TAG, System.getProperty("os.name"));
275 | if (System.getProperty("os.name").startsWith("Mac")) {
276 | NativeLibrary.addSearchPath(RuntimeUtil.getLibVlcLibraryName(), VLC_MAC);
277 | Native.loadLibrary(RuntimeUtil.getLibVlcLibraryName(), uk.co.caprica.vlcj.binding.LibVlc.class);
278 | Log.d(LOG_TAG, "VLC available");
279 | } else if (System.getProperty("os.name").startsWith("Windows")) {
280 | File vlcDirectory1 = new File(VLC_WINDOWS1);
281 | File vlcDirectory2 = new File(VLC_WINDOWS2);
282 | if (vlcDirectory1.exists()) {
283 | Log.d(LOG_TAG, "Found VLC at " + VLC_WINDOWS1);
284 | NativeLibrary.addSearchPath(RuntimeUtil.getLibVlcLibraryName(), VLC_WINDOWS1);
285 | } else if (vlcDirectory2.exists()) {
286 | Log.d(LOG_TAG, "Found VLC at " + VLC_WINDOWS2);
287 | NativeLibrary.addSearchPath(RuntimeUtil.getLibVlcLibraryName(), VLC_WINDOWS2);
288 | }
289 | Native.loadLibrary(RuntimeUtil.getLibVlcLibraryName(), uk.co.caprica.vlcj.binding.LibVlc.class);
290 | Log.d(LOG_TAG, "VLC available");
291 | } else {
292 | Native.loadLibrary(RuntimeUtil.getLibVlcLibraryName(), uk.co.caprica.vlcj.binding.LibVlc.class);
293 | Log.d(LOG_TAG, "VLC available");
294 | }
295 | } catch (Throwable ex) {
296 | // Try for other OS's
297 | try {
298 | Native.loadLibrary(RuntimeUtil.getLibVlcLibraryName(), uk.co.caprica.vlcj.binding.LibVlc.class);
299 | Log.d(LOG_TAG, "VLC available");
300 | } catch (Throwable ex2) {
301 | Log.d(LOG_TAG, "VLC not available");
302 | }
303 | }
304 | }
305 |
306 | public void doStop() {
307 | if (rampClient != null) {
308 | rampClient.closeCurrentApp(dialServer);
309 | //rampClient = null;
310 | }
311 | if (!isTranscoding) {
312 | if (embeddedServer != null) {
313 | embeddedServer.stop();
314 | embeddedServer = null;
315 | }
316 | } else {
317 | if (mediaPlayer != null) {
318 | mediaPlayer.release();
319 | mediaPlayer = null;
320 | }
321 | if (mediaPlayerFactory != null) {
322 | mediaPlayerFactory.release();
323 | mediaPlayerFactory = null;
324 | }
325 | }
326 | }
327 |
328 | public void doPlay() {
329 | if (rampClient != null) {
330 | rampClient.play();
331 | }
332 | }
333 |
334 | public void doPause() {
335 | Log.d(LOG_TAG, "doPause: " + rampClient);
336 | if (rampClient != null) {
337 | rampClient.pause();
338 | }
339 | }
340 |
341 | public DialServer getDialServer() {
342 | return dialServer;
343 | }
344 |
345 | public void setDialServer(DialServer dialServer) {
346 | this.dialServer = dialServer;
347 | if (rampClient != null) {
348 | rampClient.setDialServer(dialServer);
349 | }
350 | }
351 |
352 | }
353 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/PlaybackListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.java.caster;
15 |
16 | /**
17 | * @author leon_nicholls
18 | *
19 | */
20 | public interface PlaybackListener {
21 |
22 | public void updateTime(Playback playback, int time);
23 |
24 | public void updateDuration(Playback playback, int duration);
25 |
26 | public void updateState(Playback playback, int state);
27 | }
28 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/RampClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package com.entertailion.java.caster;
16 |
17 | import java.io.Reader;
18 | import java.io.StringReader;
19 | import java.net.URI;
20 | import java.util.UUID;
21 |
22 | import javax.xml.parsers.SAXParser;
23 | import javax.xml.parsers.SAXParserFactory;
24 |
25 | import org.apache.http.Header;
26 | import org.apache.http.HttpResponse;
27 | import org.apache.http.ProtocolException;
28 | import org.apache.http.client.methods.HttpDelete;
29 | import org.apache.http.client.methods.HttpGet;
30 | import org.apache.http.client.methods.HttpPost;
31 | import org.apache.http.entity.StringEntity;
32 | import org.apache.http.impl.client.DefaultHttpClient;
33 | import org.apache.http.impl.client.DefaultRedirectHandler;
34 | import org.apache.http.protocol.BasicHttpContext;
35 | import org.apache.http.protocol.HttpContext;
36 | import org.apache.http.util.EntityUtils;
37 | import org.java_websocket.handshake.ServerHandshake;
38 | import org.json.simple.JSONArray;
39 | import org.json.simple.JSONObject;
40 | import org.json.simple.parser.JSONParser;
41 | import org.xml.sax.InputSource;
42 | import org.xml.sax.XMLReader;
43 |
44 | /*
45 | * Manage RAMP protocol
46 | *
47 | * @author leon_nicholls
48 | */
49 | public class RampClient implements RampWebSocketListener {
50 |
51 | private static final String LOG_TAG = "RampClient";
52 |
53 | private static final String STATE_RUNNING = "running";
54 | private static final String STATE_STOPPED = "stopped";
55 |
56 | private static final String PROTOCOL_CM = "cm";
57 | private static final String PROTOCOL_RAMP = "ramp";
58 | private static final String PROTOCOL_CV = "cV";
59 | private static final String TYPE = "type";
60 | private static final String PING = "ping";
61 | private static final String PONG = "pong";
62 | private static final String ACTIVITY = "activity";
63 | private static final String ACTIVITY_MESSAGE = "message";
64 | private static final String ACTIVITY_CURRENT_TIME = "currentTime";
65 | private static final String ACTIVITY_DURATION = "duration";
66 | private static final String ACTIVITY_STATE = "state";
67 | private static final String ACTIVITY_TIME_UPDATE = "timeupdate";
68 | private static final String STATUS = "STATUS";
69 | private static final String RESPONSE = "RESPONSE";
70 | private static final String RESPONSE_STATUS = "status";
71 | private static final String RESPONSE_STATE = "state";
72 | private static final String RESPONSE_CURRENT_TIME = "current_time";
73 | private static final String RESPONSE_DURATION = "duration";
74 |
75 | private static final String HEADER_CONNECTION = "Connection";
76 | private static final String HEADER_CONNECTION_VALUE = "keep-alive";
77 | private static final String HEADER_ORIGN = "Origin";
78 | private static final String HEADER_ORIGIN_VALUE = "chrome-extension://boadgeojelhgndaghljhdicfkmllpafd";
79 | private static final String HEADER_USER_AGENT = "User-Agent";
80 | private static final String HEADER_USER_AGENT_VALUE = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36";
81 | private static final String HEADER_DNT = "DNT";
82 | private static final String HEADER_DNT_VALUE = "1";
83 | private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
84 | private static final String HEADER_ACCEPT_ENCODING_VALUE = "gzip,deflate,sdch";
85 | private static final String HEADER_ACCEPT = "Accept";
86 | private static final String HEADER_ACCEPT_VALUE = "*/*";
87 | private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language";
88 | private static final String HEADER_ACCEPT_LANGUAGE_VALUE = "en-US,en;q=0.8";
89 | private static final String HEADER_CONTENT_TYPE = "Content-Type";
90 | private static final String HEADER_CONTENT_TYPE_JSON_VALUE = "application/json";
91 | private static final String HEADER_CONTENT_TYPE_TEXT_VALUE = "text/plain";
92 |
93 | private String connectionServiceUrl;
94 | private String state;
95 | private String protocol;
96 | private String response;
97 | private boolean started;
98 | private boolean closed;
99 | private boolean doPlay;
100 |
101 | private RampWebSocketClient rampWebSocketClient;
102 | private int commandId = 1;
103 | private String app;
104 | private String activityId;
105 | private String senderId;
106 | private boolean isChromeCast;
107 | private boolean gotStatus;
108 |
109 | private Thread infoThread;
110 | private DialServer dialServer;
111 | private Playback playback;
112 | private PlaybackListener playbackListener;
113 | private DefaultHttpClient defaultHttpClient;
114 | private BasicHttpContext localContext;
115 | private CustomRedirectHandler handler;
116 |
117 | public RampClient(Playback playback, PlaybackListener playbackListener) {
118 | this.playback = playback;
119 | this.playbackListener = playbackListener;
120 | this.senderId = UUID.randomUUID().toString();
121 |
122 | defaultHttpClient = HttpRequestHelper.createHttpClient();
123 | handler = new CustomRedirectHandler();
124 | defaultHttpClient.setRedirectHandler(handler);
125 | localContext = new BasicHttpContext();
126 | }
127 |
128 | public void launchApp(String app, DialServer dialServer, String body) {
129 | this.app = app;
130 | // TODO
131 | // /this.isChromeCast = app.equals(FlingFrame.CHROMECAST);
132 | this.dialServer = dialServer;
133 | this.activityId = UUID.randomUUID().toString();
134 | if (dialServer != null) {
135 | try {
136 | String device = "http://" + dialServer.getIpAddress().getHostAddress() + ":" + dialServer.getPort();
137 | Log.d(LOG_TAG, "device=" + device);
138 | Log.d(LOG_TAG, "apps url=" + dialServer.getAppsUrl());
139 |
140 | // application instance url
141 | String location = null;
142 |
143 | // check if any app is running
144 | HttpGet httpGet = new HttpGet(dialServer.getAppsUrl());
145 | httpGet.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
146 | httpGet.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
147 | httpGet.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
148 | httpGet.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
149 | httpGet.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
150 | httpGet.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
151 | HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
152 | if (httpResponse != null) {
153 | int responseCode = httpResponse.getStatusLine().getStatusCode();
154 | Log.d(LOG_TAG, "get response code=" + httpResponse.getStatusLine().getStatusCode());
155 | if (responseCode == 204) {
156 | // nothing is running
157 | } else if (responseCode == 200) {
158 | // app is running
159 |
160 | // Need to get real URL after a redirect
161 | // http://stackoverflow.com/a/10286025/594751
162 | String lastUrl = dialServer.getAppsUrl();
163 | if (handler.lastRedirectedUri != null) {
164 | lastUrl = handler.lastRedirectedUri.toString();
165 | Log.d(LOG_TAG, "lastUrl=" + lastUrl);
166 | }
167 |
168 | String response = EntityUtils.toString(httpResponse.getEntity());
169 | Log.d(LOG_TAG, "get response=" + response);
170 | parseXml(new StringReader(response));
171 |
172 | Header[] headers = httpResponse.getAllHeaders();
173 | for (int i = 0; i < headers.length; i++) {
174 | Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
175 | }
176 |
177 | // stop the app instance
178 | HttpDelete httpDelete = new HttpDelete(lastUrl);
179 | httpResponse = defaultHttpClient.execute(httpDelete);
180 | if (httpResponse != null) {
181 | Log.d(LOG_TAG, "delete response code=" + httpResponse.getStatusLine().getStatusCode());
182 | response = EntityUtils.toString(httpResponse.getEntity());
183 | Log.d(LOG_TAG, "delete response=" + response);
184 | } else {
185 | Log.d(LOG_TAG, "no delete response");
186 | }
187 | }
188 |
189 | } else {
190 | Log.i(LOG_TAG, "no get response");
191 | return;
192 | }
193 |
194 | // Check if app is installed on device
195 | int responseCode = getAppStatus(defaultHttpClient, dialServer.getAppsUrl() + app);
196 | if (responseCode != 200) {
197 | if (responseCode == 404) {
198 | Log.e(LOG_TAG, "APP ID is invalid");
199 | }
200 | return;
201 | }
202 | parseXml(new StringReader(response));
203 | Log.d(LOG_TAG, "state=" + state);
204 |
205 | // start the app with POST
206 | HttpPost httpPost = new HttpPost(dialServer.getAppsUrl() + app);
207 | httpPost.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
208 | httpPost.setHeader(HEADER_ORIGN, HEADER_ORIGIN_VALUE);
209 | httpPost.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
210 | httpPost.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
211 | httpPost.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
212 | httpPost.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
213 | httpPost.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
214 | httpPost.setHeader(HEADER_CONTENT_TYPE, HEADER_CONTENT_TYPE_TEXT_VALUE);
215 | if (isChromeCast) {
216 | // httpPost.setEntity(new
217 | // StringEntity("v=release-d4fa0a24f89ec5ba83f7bf3324282c8d046bf612&id=local%3A1&idle=windowclose"));
218 | httpPost.setEntity(new StringEntity("v=release-d4fa0a24f89ec5ba83f7bf3324282c8d046bf612&id=local%3A1"));
219 | }
220 | if (body!=null) {
221 | httpPost.setEntity(new StringEntity(body)); // http://www.youtube.com/watch?v=cKG5HDyTW8o
222 | }
223 |
224 | httpResponse = defaultHttpClient.execute(httpPost, localContext);
225 | if (httpResponse != null) {
226 | Log.d(LOG_TAG, "post response code=" + httpResponse.getStatusLine().getStatusCode());
227 | response = EntityUtils.toString(httpResponse.getEntity());
228 | Log.d(LOG_TAG, "post response=" + response);
229 | Header[] headers = httpResponse.getHeaders("LOCATION");
230 | if (headers.length > 0) {
231 | location = headers[0].getValue();
232 | Log.d(LOG_TAG, "post response location=" + location);
233 | }
234 |
235 | headers = httpResponse.getAllHeaders();
236 | for (int i = 0; i < headers.length; i++) {
237 | Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
238 | }
239 | } else {
240 | Log.i(LOG_TAG, "no post response");
241 | return;
242 | }
243 |
244 | getWebSocket(app);
245 | } catch (Exception e) {
246 | Log.e(LOG_TAG, "launchApp", e);
247 | }
248 | } else {
249 | Log.d(LOG_TAG, "launchApp: dialserver null");
250 | }
251 | }
252 |
253 | private void getWebSocket(String app) {
254 | try {
255 | int responseCode = 0;
256 | // Keep trying to get the app status until the
257 | // connection service URL is available
258 | state = STATE_STOPPED;
259 | do {
260 | responseCode = getAppStatus(defaultHttpClient, dialServer.getAppsUrl() + app);
261 | if (responseCode != 200) {
262 | if (responseCode == 404) {
263 | Log.e(LOG_TAG, "APP ID is invalid");
264 | }
265 | break;
266 | }
267 | parseXml(new StringReader(response));
268 | Log.d(LOG_TAG, "state=" + state);
269 | Log.d(LOG_TAG, "connectionServiceUrl=" + connectionServiceUrl);
270 | Log.d(LOG_TAG, "protocol=" + protocol);
271 | try {
272 | Thread.sleep(1000);
273 | } catch (Exception e) {
274 | }
275 | } while (state.equals(STATE_RUNNING) && connectionServiceUrl == null);
276 |
277 | if (connectionServiceUrl == null) {
278 | Log.i(LOG_TAG, "connectionServiceUrl is null");
279 | return; // oops, something went wrong
280 | }
281 |
282 | // get the websocket URL
283 | String webSocketAddress = null;
284 | HttpPost httpPost = new HttpPost(connectionServiceUrl); // "http://192.168.0.17:8008/connection/YouTube"
285 | httpPost.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
286 | httpPost.setHeader(HEADER_ORIGN, HEADER_ORIGIN_VALUE);
287 | httpPost.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
288 | httpPost.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
289 | httpPost.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
290 | httpPost.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
291 | httpPost.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
292 | httpPost.setHeader(HEADER_CONTENT_TYPE, HEADER_CONTENT_TYPE_JSON_VALUE);
293 | httpPost.setEntity(new StringEntity("{\"channel\":0,\"senderId\":{\"appName\":\"" + app + "\", \"senderId\":\"" + senderId + "\"}}"));
294 |
295 | HttpResponse httpResponse = defaultHttpClient.execute(httpPost, localContext);
296 | if (httpResponse != null) {
297 | responseCode = httpResponse.getStatusLine().getStatusCode();
298 | Log.d(LOG_TAG, "post response code=" + responseCode);
299 | if (responseCode == 200) {
300 | // should return JSON payload
301 | response = EntityUtils.toString(httpResponse.getEntity());
302 | Log.d(LOG_TAG, "post response=" + response);
303 | Header[] headers = httpResponse.getAllHeaders();
304 | for (int i = 0; i < headers.length; i++) {
305 | Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
306 | }
307 |
308 | // http://code.google.com/p/json-simple/
309 | JSONParser parser = new JSONParser();
310 | try {
311 | Object obj = parser.parse(new StringReader(response)); // {"URL":"ws://192.168.0.17:8008/session?33","pingInterval":0}
312 | JSONObject jsonObject = (JSONObject) obj;
313 | webSocketAddress = (String) jsonObject.get("URL");
314 | Log.d(LOG_TAG, "webSocketAddress: " + webSocketAddress);
315 | long pingInterval = (Long) jsonObject.get("pingInterval"); // TODO
316 | } catch (Exception e) {
317 | Log.e(LOG_TAG, "parse JSON", e);
318 | }
319 | }
320 | } else {
321 | Log.i(LOG_TAG, "no post response");
322 | return;
323 | }
324 |
325 | // Make a web socket connection for doing RAMP
326 | // to control media playback
327 | this.started = false;
328 | this.closed = false;
329 | this.gotStatus = false;
330 | if (webSocketAddress != null) {
331 | // https://github.com/TooTallNate/Java-WebSocket
332 | URI uri = URI.create(webSocketAddress);
333 |
334 | rampWebSocketClient = new RampWebSocketClient(uri, this);
335 |
336 | new Thread(new Runnable() {
337 | public void run() {
338 | Thread t = new Thread(rampWebSocketClient);
339 | t.start();
340 | try {
341 | t.join();
342 | } catch (InterruptedException e1) {
343 | e1.printStackTrace();
344 | } finally {
345 | rampWebSocketClient.close();
346 | }
347 | }
348 | }).start();
349 | } else {
350 | Log.i(LOG_TAG, "webSocketAddress is null");
351 | }
352 | } catch (Exception e) {
353 | Log.e(LOG_TAG, "getWebSocket", e);
354 | }
355 | }
356 |
357 | public void closeCurrentApp(DialServer dialServer) {
358 | if (dialServer != null) {
359 | try {
360 | DefaultHttpClient defaultHttpClient = HttpRequestHelper.createHttpClient();
361 | CustomRedirectHandler handler = new CustomRedirectHandler();
362 | defaultHttpClient.setRedirectHandler(handler);
363 | BasicHttpContext localContext = new BasicHttpContext();
364 |
365 | // check if any app is running
366 | HttpGet httpGet = new HttpGet(dialServer.getAppsUrl());
367 | httpGet.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
368 | httpGet.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
369 | httpGet.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
370 | httpGet.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
371 | httpGet.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
372 | httpGet.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
373 | HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
374 | if (httpResponse != null) {
375 | int responseCode = httpResponse.getStatusLine().getStatusCode();
376 | Log.d(LOG_TAG, "get response code=" + httpResponse.getStatusLine().getStatusCode());
377 | if (responseCode == 204) {
378 | // nothing is running
379 | } else if (responseCode == 200) {
380 | // app is running
381 |
382 | // Need to get real URL after a redirect
383 | // http://stackoverflow.com/a/10286025/594751
384 | String lastUrl = dialServer.getAppsUrl();
385 | if (handler.lastRedirectedUri != null) {
386 | lastUrl = handler.lastRedirectedUri.toString();
387 | Log.d(LOG_TAG, "lastUrl=" + lastUrl);
388 | }
389 |
390 | String response = EntityUtils.toString(httpResponse.getEntity());
391 | Log.d(LOG_TAG, "get response=" + response);
392 | parseXml(new StringReader(response));
393 |
394 | Header[] headers = httpResponse.getAllHeaders();
395 | for (int i = 0; i < headers.length; i++) {
396 | Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
397 | }
398 |
399 | // stop the app instance
400 | HttpDelete httpDelete = new HttpDelete(lastUrl);
401 | httpResponse = defaultHttpClient.execute(httpDelete);
402 | if (httpResponse != null) {
403 | Log.d(LOG_TAG, "delete response code=" + httpResponse.getStatusLine().getStatusCode());
404 | response = EntityUtils.toString(httpResponse.getEntity());
405 | Log.d(LOG_TAG, "delete response=" + response);
406 | } else {
407 | Log.d(LOG_TAG, "no delete response");
408 | }
409 | }
410 |
411 | } else {
412 | Log.i(LOG_TAG, "no get response");
413 | return;
414 | }
415 | } catch (Exception e) {
416 | Log.e(LOG_TAG, "closeCurrentApp", e);
417 | }
418 | } else {
419 | Log.d(LOG_TAG, "closeCurrentApp: dialserver null");
420 | }
421 | }
422 |
423 | public String getCurrentApp(DialServer dialServer) {
424 | if (dialServer != null) {
425 | try {
426 | DefaultHttpClient defaultHttpClient = HttpRequestHelper.createHttpClient();
427 | CustomRedirectHandler handler = new CustomRedirectHandler();
428 | defaultHttpClient.setRedirectHandler(handler);
429 | BasicHttpContext localContext = new BasicHttpContext();
430 |
431 | // check if any app is running
432 | HttpGet httpGet = new HttpGet(dialServer.getAppsUrl());
433 | httpGet.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
434 | httpGet.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
435 | httpGet.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
436 | httpGet.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
437 | httpGet.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
438 | httpGet.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
439 | HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
440 | if (httpResponse != null) {
441 | int responseCode = httpResponse.getStatusLine().getStatusCode();
442 | Log.d(LOG_TAG, "get response code=" + httpResponse.getStatusLine().getStatusCode());
443 | if (responseCode == 204) {
444 | // nothing is running
445 | } else if (responseCode == 200) {
446 | // app is running
447 |
448 | // Need to get real URL after a redirect
449 | // http://stackoverflow.com/a/10286025/594751
450 | String lastUrl = dialServer.getAppsUrl();
451 | if (handler.lastRedirectedUri != null) {
452 | lastUrl = handler.lastRedirectedUri.toString();
453 | Log.d(LOG_TAG, "lastUrl=" + lastUrl);
454 | String[] parts = lastUrl.split("/");
455 | if (parts.length > 0) {
456 | return parts[parts.length - 1];
457 | }
458 | }
459 | }
460 |
461 | } else {
462 | Log.i(LOG_TAG, "no get response");
463 | }
464 | } catch (Exception e) {
465 | Log.e(LOG_TAG, "getCurrentApp", e);
466 | }
467 | } else {
468 | Log.d(LOG_TAG, "getCurrentApp: dialserver null");
469 | }
470 | return null;
471 | }
472 |
473 | /**
474 | * Do HTTP GET for app status to determine response code and response body
475 | *
476 | * @param defaultHttpClient
477 | * @param url
478 | * @return
479 | */
480 | private int getAppStatus(DefaultHttpClient defaultHttpClient, String url) {
481 | int responseCode = 200;
482 | try {
483 | HttpGet httpGet = new HttpGet(url);
484 | HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
485 | if (httpResponse != null) {
486 | responseCode = httpResponse.getStatusLine().getStatusCode();
487 | Log.d(LOG_TAG, "get response code=" + responseCode);
488 | response = EntityUtils.toString(httpResponse.getEntity());
489 | Log.d(LOG_TAG, "get response=" + response);
490 | } else {
491 | Log.i(LOG_TAG, "no get response");
492 | }
493 | } catch (Exception e) {
494 | Log.e(LOG_TAG, "getAppStatus", e);
495 | }
496 | return responseCode;
497 | }
498 |
499 | private void parseXml(Reader reader) {
500 | try {
501 | InputSource inStream = new org.xml.sax.InputSource();
502 | inStream.setCharacterStream(reader);
503 | SAXParserFactory spf = SAXParserFactory.newInstance();
504 | SAXParser sp = spf.newSAXParser();
505 | XMLReader xr = sp.getXMLReader();
506 | AppHandler appHandler = new AppHandler();
507 | xr.setContentHandler(appHandler);
508 | xr.parse(inStream);
509 |
510 | connectionServiceUrl = appHandler.getConnectionServiceUrl();
511 | state = appHandler.getState();
512 | protocol = appHandler.getProtocol();
513 | } catch (Exception e) {
514 | Log.e(LOG_TAG, "parse device description", e);
515 | }
516 | }
517 |
518 | /**
519 | * Custom HTTP redirection handler to keep track of the redirected URL
520 | * ChromeCast web server will redirect "/apps" to "/apps/YouTube" if that is
521 | * the active/last app
522 | *
523 | */
524 | public class CustomRedirectHandler extends DefaultRedirectHandler {
525 |
526 | public URI lastRedirectedUri;
527 |
528 | @Override
529 | public boolean isRedirectRequested(HttpResponse response, HttpContext context) {
530 |
531 | return super.isRedirectRequested(response, context);
532 | }
533 |
534 | @Override
535 | public URI getLocationURI(HttpResponse response, HttpContext context) throws ProtocolException {
536 |
537 | lastRedirectedUri = super.getLocationURI(response, context);
538 |
539 | return lastRedirectedUri;
540 | }
541 |
542 | }
543 |
544 | // RampWebSocketListener callbacks
545 | public void onMessage(String message) {
546 | Log.d(LOG_TAG, "onMessage: message" + message);
547 |
548 | // http://code.google.com/p/json-simple/
549 | JSONParser parser = new JSONParser();
550 | try {
551 | Object obj = parser.parse(new StringReader(message));
552 | JSONArray array = (JSONArray) obj;
553 | if (array.get(0).equals(PROTOCOL_CM)) {
554 | Log.d(LOG_TAG, PROTOCOL_CM);
555 | JSONObject body = (JSONObject) array.get(1);
556 | // ["cm",{"type":"ping"}]
557 | if (body.get(TYPE).equals(PING)) {
558 | rampWebSocketClient.send("[\"cm\",{\"type\":\"pong\"}]");
559 | }
560 | } else if (array.get(0).equals(PROTOCOL_RAMP)) {
561 | // ["ramp",{"cmd_id":0,"type":"STATUS","status":{"event_sequence":2,"state":0}}]
562 | Log.d(LOG_TAG, PROTOCOL_RAMP);
563 | JSONObject body = (JSONObject) array.get(1);
564 | if (body.get(TYPE).equals(STATUS)) {
565 | // Long cmd_id = (Long)body.get("cmd_id");
566 | // commandId = cmd_id.intValue();
567 | if (!gotStatus) {
568 | gotStatus = true;
569 | // rampWebSocketClient.send("[\"ramp\",{\"type\":\"LOAD\",\"cmd_id\":"+commandId+",\"autoplay\":true}] ");
570 | // commandId++;
571 | }
572 | } else if (body.get(TYPE).equals(RESPONSE)) {
573 | // ["ramp",{"cmd_id":7,"type":"RESPONSE","status":{"event_sequence":38,"state":2,"content_id":"http://192.168.0.50:8080/video.mp4","current_time":6.465110778808594,
574 | // "duration":27.37066650390625,"volume":1,"muted":false,"time_progress":true,"title":"Video"}}]
575 | JSONObject status = (JSONObject) body.get(RESPONSE_STATUS);
576 | if (status.get(RESPONSE_CURRENT_TIME) instanceof Double) {
577 | Double current_time = (Double) status.get(RESPONSE_CURRENT_TIME);
578 | if (current_time != null) {
579 | if (playbackListener != null) {
580 | playbackListener.updateTime(playback, current_time.intValue());
581 | }
582 | }
583 | } else {
584 | Long current_time = (Long) status.get(RESPONSE_CURRENT_TIME);
585 | if (current_time != null) {
586 | if (playbackListener != null) {
587 | playbackListener.updateTime(playback, current_time.intValue());
588 | }
589 | }
590 | }
591 | if (status.get(RESPONSE_DURATION) instanceof Double) {
592 | Double duration = (Double) status.get(RESPONSE_DURATION);
593 | if (duration != null) {
594 | if (playbackListener != null) {
595 | playbackListener.updateDuration(playback, duration.intValue());
596 | }
597 | }
598 | } else {
599 | Long duration = (Long) status.get(RESPONSE_DURATION);
600 | if (duration != null) {
601 | if (playbackListener != null) {
602 | playbackListener.updateDuration(playback, duration.intValue());
603 | }
604 | }
605 | }
606 | Long state = (Long) status.get(RESPONSE_STATE);
607 | if (playbackListener != null) {
608 | playbackListener.updateState(playback, state.intValue());
609 | }
610 | }
611 | } else if (array.get(0).equals(PROTOCOL_CV)) { // ChromeCast default
612 | // receiver events
613 | Log.d(LOG_TAG, PROTOCOL_CV);
614 | JSONObject body = (JSONObject) array.get(1);
615 | if (body.get(TYPE).equals(ACTIVITY)) {
616 | // ["cv",{"type":"activity","message":{"type":"timeupdate","activityId":"d82cede3-ec23-4f73-8abc-343dd9ca6dbb","state":{"mediaUrl":"http://192.168.0.50:8087/cast.webm","videoUrl":"http://192.168.0.50:8087/cast.webm",
617 | // "currentTime":20.985000610351562,"duration":null,"pause":false,"muted":false,"volume":1,"paused":false}}}]
618 | JSONObject activityMessage = (JSONObject) body.get(ACTIVITY_MESSAGE);
619 | if (activityMessage != null) {
620 | JSONObject activityMessageType = (JSONObject) activityMessage.get(TYPE);
621 | if (activityMessageType.equals(ACTIVITY_TIME_UPDATE)) {
622 | JSONObject activityMessageTypeState = (JSONObject) activityMessage.get(ACTIVITY_STATE);
623 | if (activityMessageTypeState.get(RESPONSE_CURRENT_TIME) instanceof Double) {
624 | Double current_time = (Double) activityMessageTypeState.get(ACTIVITY_CURRENT_TIME);
625 | Double duration = (Double) activityMessageTypeState.get(ACTIVITY_DURATION);
626 | if (duration != null) {
627 | if (playbackListener != null) {
628 | playbackListener.updateDuration(playback, duration.intValue());
629 | }
630 | }
631 | if (current_time != null) {
632 | if (playbackListener != null) {
633 | playbackListener.updateTime(playback, current_time.intValue());
634 | }
635 | }
636 | } else {
637 | Long current_time = (Long) activityMessageTypeState.get(ACTIVITY_CURRENT_TIME);
638 | Double duration = (Double) activityMessageTypeState.get(ACTIVITY_DURATION);
639 | if (duration != null) {
640 | if (playbackListener != null) {
641 | playbackListener.updateDuration(playback, duration.intValue());
642 | }
643 | }
644 | if (current_time != null) {
645 | if (playbackListener != null) {
646 | playbackListener.updateTime(playback, current_time.intValue());
647 | }
648 | }
649 | }
650 | }
651 | }
652 |
653 | }
654 | }
655 | } catch (Exception e) {
656 | Log.e(LOG_TAG, "parse JSON", e);
657 | }
658 | }
659 |
660 | public void onError(Exception ex) {
661 | Log.d(LOG_TAG, "onError: ex" + ex);
662 | ex.printStackTrace();
663 |
664 | started = false;
665 | closed = true;
666 |
667 | infoThread.interrupt();
668 | }
669 |
670 | public void onOpen(ServerHandshake handshake) {
671 | Log.d(LOG_TAG, "onOpen: handshake" + handshake);
672 |
673 | started = true;
674 | closed = false;
675 |
676 | if (infoThread != null) {
677 | infoThread.interrupt();
678 | }
679 |
680 | infoThread = new Thread(new Runnable() {
681 | public void run() {
682 | while (started && !closed) {
683 | try {
684 | if (gotStatus) {
685 | rampWebSocketClient.send("[\"ramp\",{\"type\":\"INFO\",\"cmd_id\":" + commandId + "}]");
686 | commandId++;
687 | }
688 | try {
689 | Thread.sleep(1000);
690 | } catch (InterruptedException e) {
691 | }
692 | } catch (Exception e) {
693 | Log.e(LOG_TAG, "infoThread", e);
694 | }
695 | }
696 | }
697 | });
698 | infoThread.start();
699 | }
700 |
701 | public void onClose(int code, String reason, boolean remote) {
702 | Log.d(LOG_TAG, "onClose: code" + code + ", reason=" + reason + ", remote=" + remote);
703 |
704 | closed = true;
705 | started = false;
706 |
707 | infoThread.interrupt();
708 |
709 | if (playbackListener != null) {
710 | playbackListener.updateTime(playback, 0);
711 | }
712 | }
713 |
714 | // Media playback controls
715 | public void play() {
716 | Log.d(LOG_TAG, "play: " + rampWebSocketClient);
717 | if (rampWebSocketClient == null) {
718 | String app = getCurrentApp(dialServer);
719 | Log.d(LOG_TAG, "play: currentApp=" + app);
720 | getWebSocket(app);
721 | }
722 | if (rampWebSocketClient != null) {
723 | rampWebSocketClient.send("[\"ramp\",{\"type\":\"PLAY\", \"cmd_id\":" + commandId + "}]");
724 | commandId++;
725 | }
726 | }
727 |
728 | public void play(int position) {
729 | Log.d(LOG_TAG, "play: " + rampWebSocketClient);
730 | if (rampWebSocketClient == null) {
731 | String app = getCurrentApp(dialServer);
732 | Log.d(LOG_TAG, "play: currentApp=" + app);
733 | getWebSocket(app);
734 | }
735 | if (rampWebSocketClient != null) {
736 | rampWebSocketClient.send("[\"ramp\",{\"type\":\"PLAY\", \"cmd_id\":" + commandId + ", \"position\":" + position + "}]");
737 | commandId++;
738 | }
739 | }
740 |
741 | public void pause() {
742 | Log.d(LOG_TAG, "pause: " + rampWebSocketClient);
743 | if (rampWebSocketClient == null) {
744 | String app = getCurrentApp(dialServer);
745 | Log.d(LOG_TAG, "stop: currentApp=" + app);
746 | getWebSocket(app);
747 | }
748 | if (rampWebSocketClient != null) {
749 | rampWebSocketClient.send("[\"ramp\",{\"type\":\"STOP\", \"cmd_id\":" + commandId + "}]");
750 | commandId++;
751 | }
752 | }
753 |
754 | public void stop() {
755 | // ChromeCast app stop behaves like pause
756 | /*
757 | * if (rampWebSocketClient != null) {
758 | * rampWebSocketClient.send("[\"ramp\",{\"type\":\"STOP\", \"cmd_id\":"
759 | * + commandId + "}]"); commandId++; }
760 | */
761 | // Close the current app
762 | closeCurrentApp(dialServer);
763 | }
764 |
765 | public void info() {
766 | if (rampWebSocketClient != null) {
767 | rampWebSocketClient.send("[\"ramp\",{\"type\":\"INFO\", \"cmd_id\":" + commandId + "}]");
768 | commandId++;
769 | }
770 | }
771 |
772 | // Load media
773 | public void load(String url) {
774 | Log.d(LOG_TAG, "load: " + rampWebSocketClient);
775 | if (rampWebSocketClient != null) {
776 | if (isChromeCast) {
777 | rampWebSocketClient
778 | .send("[\"cv\",{\"type\":\"launch_service\",\"message\":{\"action\":\"launch\",\"activityType\":\"video_playback\",\"activityId\":\""
779 | + activityId + "\",\"senderId\":\"" + senderId
780 | + "\",\"receiverId\":\"local:1\",\"disconnectPolicy\":\"stop\",\"initParams\":{\"mediaUrl\":\"" + url
781 | + "\",\"currentTime\":0,\"duration\":0,\"pause\":false,\"muted\":false,\"volume\":1}}}]");
782 | } else {
783 | rampWebSocketClient.send("[\"ramp\",{\"title\":\"Video\",\"src\":\"" + url + "\",\"type\":\"LOAD\",\"cmd_id\":" + commandId
784 | + ",\"autoplay\":true}]");
785 | commandId++;
786 | }
787 | }
788 | }
789 |
790 | public void volume(float value) {
791 | if (rampWebSocketClient != null) {
792 | // ["ramp",{"volume":0.5,"type":"VOLUME","cmd_id":6}]
793 | rampWebSocketClient.send("[\"ramp\",{\"type\":\"VOLUME\", \"cmd_id\":" + commandId + ", \"volume\":" + value + "}]");
794 | commandId++;
795 | }
796 | }
797 |
798 | // Web socket status
799 | public boolean isStarted() {
800 | return started;
801 | }
802 |
803 | public boolean isClosed() {
804 | return closed;
805 | }
806 |
807 | public void setDialServer(DialServer dialServer) {
808 | this.dialServer = dialServer;
809 | }
810 | }
811 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/RampWebSocketClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.entertailion.java.caster;
18 |
19 | import java.net.URI;
20 | import java.nio.ByteBuffer;
21 | import java.util.LinkedList;
22 |
23 | import org.java_websocket.WebSocketImpl;
24 | import org.java_websocket.client.WebSocketClient;
25 | import org.java_websocket.drafts.Draft_17;
26 | import org.java_websocket.handshake.ServerHandshake;
27 |
28 | /*
29 | * Web socket client for RAMP commands
30 | * https://github.com/TooTallNate/Java-WebSocket
31 | *
32 | * @author leon_nicholls
33 | */
34 | public class RampWebSocketClient extends WebSocketClient {
35 |
36 | private static final String LOG_TAG = "RampWebSocketClient";
37 |
38 | private RampWebSocketListener rampWebSocketListener;
39 |
40 | // Queue messages until the connection is ready
41 | private LinkedList messageQueue = new LinkedList();
42 |
43 | private boolean ready;
44 |
45 | public RampWebSocketClient(URI uri, RampWebSocketListener rampWebSocketListener) {
46 | super(uri, new Draft_17());
47 | this.rampWebSocketListener = rampWebSocketListener;
48 | WebSocketImpl.DEBUG = Log.getVerbose();
49 | }
50 |
51 | @Override
52 | public void onMessage(String message) {
53 | rampWebSocketListener.onMessage(message);
54 | }
55 |
56 | @Override
57 | public void onMessage(ByteBuffer blob) {
58 | getConnection().send(blob);
59 | }
60 |
61 | @Override
62 | public void onError(Exception ex) {
63 | rampWebSocketListener.onError(ex);
64 | }
65 |
66 | @Override
67 | public void onOpen(ServerHandshake handshake) {
68 | ready = true;
69 | for (String msg : messageQueue) {
70 | super.send(msg);
71 | }
72 | messageQueue.clear();
73 | rampWebSocketListener.onOpen(handshake);
74 | }
75 |
76 | @Override
77 | public void onClose(int code, String reason, boolean remote) {
78 | rampWebSocketListener.onClose(code, reason, remote);
79 | }
80 |
81 | @Override
82 | public void send(String message) {
83 | Log.d(LOG_TAG, "message=" + message);
84 | if (ready) {
85 | for (String msg : messageQueue) {
86 | super.send(msg);
87 | }
88 | messageQueue.clear();
89 | super.send(message);
90 | } else {
91 | messageQueue.add(message);
92 | }
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/RampWebSocketListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.entertailion.java.caster;
17 |
18 | import org.java_websocket.handshake.ServerHandshake;
19 |
20 | /*
21 | * Callback for web socket events
22 | *
23 | * @author leon_nicholls
24 | */
25 | public interface RampWebSocketListener {
26 | public void onMessage(String message);
27 |
28 | public void onError(Exception ex);
29 |
30 | public void onOpen(ServerHandshake handshake);
31 |
32 | public void onClose(int code, String reason, boolean remote);
33 | }
34 |
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/TrackedDialServers.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.java.caster;
15 |
16 | import java.net.InetAddress;
17 | import java.util.Comparator;
18 | import java.util.HashMap;
19 | import java.util.Iterator;
20 | import java.util.Map;
21 | import java.util.SortedSet;
22 | import java.util.TreeSet;
23 |
24 | /*
25 | * Keep track of the found DIAL servers
26 | *
27 | * @author leon_nicholls
28 | */
29 | public class TrackedDialServers implements Iterable {
30 | private static final String LOG_TAG = "TrackedDialServers";
31 |
32 | private final Map serversByAddress;
33 | private final SortedSet servers;
34 | private DialServer[] serverArray;
35 |
36 | private static Comparator COMPARATOR = new Comparator() {
37 | public int compare(DialServer remote1, DialServer remote2) {
38 | if (remote1.getFriendlyName() != null && remote2.getFriendlyName() != null) {
39 | int result = remote1.getFriendlyName().compareToIgnoreCase(remote2.getFriendlyName());
40 | if (result != 0) {
41 | return result;
42 | }
43 | }
44 | return remote1.getIpAddress().getHostAddress().compareTo(remote2.getIpAddress().getHostAddress());
45 | }
46 | };
47 |
48 | TrackedDialServers() {
49 | serversByAddress = new HashMap();
50 | servers = new TreeSet(COMPARATOR);
51 | }
52 |
53 | public boolean add(DialServer DialServer) {
54 | InetAddress address = DialServer.getIpAddress();
55 | if (!serversByAddress.containsKey(address)) {
56 | serversByAddress.put(address, DialServer);
57 | servers.add(DialServer);
58 | serverArray = null;
59 | return true;
60 | }
61 | return false;
62 | }
63 |
64 | public int size() {
65 | return servers.size();
66 | }
67 |
68 | public DialServer get(int index) {
69 | return getServerArray()[index];
70 | }
71 |
72 | private DialServer[] getServerArray() {
73 | if (serverArray == null) {
74 | serverArray = servers.toArray(new DialServer[0]);
75 | }
76 | return serverArray;
77 | }
78 |
79 | public Iterator iterator() {
80 | return servers.iterator();
81 | }
82 |
83 | public DialServer findDialServer(DialServer DialServer) {
84 | DialServer byIp = serversByAddress.get(DialServer.getIpAddress());
85 | if (byIp != null && byIp.getFriendlyName().equals(DialServer.getFriendlyName())) {
86 | return byIp;
87 | }
88 |
89 | for (DialServer server : servers) {
90 | Log.d(LOG_TAG, "New server: " + server);
91 | if (DialServer.getFriendlyName().equals(server.getFriendlyName())) {
92 | return server;
93 | }
94 | }
95 | return byIp;
96 | }
97 |
98 | public DialServer findDialServer(InetAddress ipAddress) {
99 | return serversByAddress.get(ipAddress);
100 | }
101 |
102 | public TrackedDialServers clone() {
103 | TrackedDialServers trackedServers = new TrackedDialServers();
104 | for (DialServer server : servers) {
105 | trackedServers.add(server.clone());
106 | }
107 | return trackedServers;
108 | }
109 | }
--------------------------------------------------------------------------------
/src/com/entertailion/java/caster/WebListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.java.caster;
15 |
16 | import java.util.Properties;
17 |
18 | import com.entertailion.java.caster.HttpServer.Response;
19 |
20 | /**
21 | * @author leon_nicholls
22 | *
23 | */
24 | public interface WebListener {
25 |
26 | public Response handleRequest(String uri, String method, Properties header, Properties parms);
27 |
28 | public String[] uriPrefixes();
29 |
30 | }
31 |
--------------------------------------------------------------------------------