View Javadoc

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 		// the 'HEAD' line should look like
117 		// HEAD /path/ HTTP/1.0
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 		// the 'GET' line should look like
137 		// GET /path/ HTTP/1.1
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 }