1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 package sk.baka.ambient.playerservice;
20
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.net.Socket;
25 import java.net.URL;
26
27 import sk.baka.ambient.AmbientApplication;
28 import sk.baka.ambient.commons.IOUtils;
29 import sk.baka.ambient.commons.MiscUtils;
30 import sk.baka.ambient.commons.ServerHttpException;
31 import sk.baka.ambient.commons.SocketServer;
32 import sk.baka.ambient.stream.shoutcast.ShoutcastInputStream;
33 import android.media.MediaPlayer;
34
35 /***
36 * <p>
37 * This class acts as a mp3 data feed for the {@link MediaPlayer} component. It
38 * acts as a very simple http server (handles GET requests on http://localhost:
39 * {@value #PORT}/). The following paths are supported:
40 * </p>
41 * <ul>
42 * <li>/shoutcast/host/port/path - a shoutcast radio located at
43 * http://host:port/path</li>
44 * </ul>
45 * <p>
46 * The port is opened immediately when the object is created. Use the
47 * {@link #close()} method to close and cleanup the server.
48 * </p>
49 *
50 * @author Martin Vysny
51 */
52 public final class StreamerServer extends SocketServer {
53 /***
54 * The port to listen on.
55 */
56 public static final int PORT = 5412;
57
58 /***
59 * Owning player service.
60 */
61 private final PlayerService service;
62
63 /***
64 * Returns an URL which accesses given shoutcast radio.
65 *
66 * @param shoutcast
67 * the shoutcast radio URL
68 * @return this relay server URL.
69 */
70 public static String getShoutcastStream(final URL shoutcast) {
71 final int port = shoutcast.getPort() < 0 ? 80 : shoutcast.getPort();
72 return "http://localhost:" + PORT + "/shoutcast/" + shoutcast.getHost()
73 + "/" + port + shoutcast.getPath();
74 }
75
76 /***
77 * Creates new server instance and opens the listen port.
78 *
79 * @param service
80 * Owning player service.
81 */
82 public StreamerServer(final PlayerService service) {
83 super();
84 this.service = service;
85 }
86
87 @Override
88 protected void handleRequest(Socket socket, InputStream in, OutputStream out)
89 throws IOException, ServerHttpException {
90 try {
91 final URL url = getURL(in, out);
92 if (url == null) {
93 return;
94 }
95 final InputStream shoutcast = new ShoutcastInputStream(url, service);
96 IOUtils.copy(shoutcast, out, 8192);
97 } catch (final RuntimeException e) {
98 if (!Thread.currentThread().isInterrupted()) {
99 AmbientApplication.getHandler().post(new Runnable() {
100 public void run() {
101 service.invokeError(e.getMessage(), e, false);
102 }
103 });
104 }
105 }
106 }
107
108 private URL getURL(final InputStream socketIn, final OutputStream socketOut)
109 throws IOException, ServerHttpException {
110 String request = MiscUtils.emptyIfNull(IOUtils.readLine(socketIn));
111 if (Thread.currentThread().isInterrupted()) {
112 return null;
113 }
114 IOUtils.HttpRequest req = IOUtils.parseRequest(request);
115 IOUtils.readRequest(socketIn);
116
117
118 while (req.method.equals("HEAD")) {
119 IOUtils.writeHttpResponse(req.version, 200, true, 100000000,
120 "audio/mpeg", socketOut);
121 IOUtils.writeLine(socketOut);
122 request = IOUtils.readLine(socketIn);
123 if (Thread.currentThread().isInterrupted()) {
124 return null;
125 }
126 if (MiscUtils.isEmpty(request)) {
127 return null;
128 }
129 req = IOUtils.parseRequest(request);
130 IOUtils.readRequest(socketIn);
131 }
132 if (!req.method.equals("GET")) {
133 throw new ServerHttpException(501, "Unsupported method: "
134 + req.method);
135 }
136
137
138 String path = req.path;
139 if (path.startsWith("/")) {
140 path = path.substring(1);
141 }
142 final URL result;
143 if (path.startsWith("shoutcast/")) {
144 result = parseShoutcastPath(path.substring(10));
145 } else {
146 throw new ServerHttpException(404, "Unhandled URL: " + path);
147 }
148 IOUtils.writeHttpResponse(req.version, 200, false, 100000000,
149 "audio/mpeg", socketOut);
150 IOUtils.writeLine(socketOut);
151 return result;
152 }
153
154 private URL parseShoutcastPath(String path) throws IOException {
155 String pathRemainder = path;
156 int nextSlash = pathRemainder.indexOf('/');
157 if (nextSlash < 0) {
158 throw new IOException("Missing port");
159 }
160 final String host = pathRemainder.substring(0, nextSlash);
161 pathRemainder = pathRemainder.substring(nextSlash + 1);
162 nextSlash = pathRemainder.indexOf('/');
163 if (nextSlash < 0)
164 nextSlash = pathRemainder.length();
165 final int port = Integer
166 .parseInt(pathRemainder.substring(0, nextSlash));
167 final String urlPath = pathRemainder.substring(nextSlash);
168 return new URL("http", host, port, urlPath);
169 }
170 }