View Javadoc

1   package org.musicontroller.streaming;
2   
3   import java.io.File;
4   import java.io.FileInputStream;
5   import java.io.FileNotFoundException;
6   import java.io.IOException;
7   import java.io.InputStream;
8   import java.io.OutputStream;
9   import java.net.SocketException;
10  import java.util.Map;
11  
12  import javax.servlet.http.HttpServletRequest;
13  
14  import org.apache.log4j.Logger;
15  import org.apache.tapestry.IRequestCycle;
16  import org.apache.tapestry.engine.IEngineService;
17  import org.apache.tapestry.engine.ILink;
18  import org.apache.tapestry.services.LinkFactory;
19  import org.apache.tapestry.services.ServiceConstants;
20  import org.apache.tapestry.util.ContentType;
21  import org.apache.tapestry.web.WebResponse;
22  import org.hibernate.HibernateException;
23  import org.hibernate.Session;
24  import org.hibernate.SessionFactory;
25  import org.hibernate.Transaction;
26  import org.musicontroller.DJ;
27  import org.musicontroller.MusiControllerException;
28  import org.musicontroller.core.Song;
29  import org.musicontroller.security.IUser;
30  import org.springframework.orm.hibernate3.SessionHolder;
31  import org.springframework.transaction.support.TransactionSynchronizationManager;
32  
33  public class StreamService implements IEngineService {
34  	private static final Logger log = Logger.getLogger(StreamService.class);
35  
36  	HttpServletRequest _req = null;
37  	public HttpServletRequest getServletRequest() {
38  		return _req;
39  	}
40  	public void setServletRequest(HttpServletRequest req) {
41  		_req = req;
42  	}
43  
44  	public static final String SERVICE_NAME = "stream";
45  
46  	/**
47  	 * Indicates the metadata-interval in bytes. After every interval, the metadata
48  	 * is copied to the stream.<br>This value is (mis)used for the buffersize of the
49  	 * streaming-mechanism too.
50  	 */
51  	private static final int BUFFER_SIZE = 16384;
52  
53      private LinkFactory _linkFactory;
54      private WebResponse _response;
55  	
56  	@SuppressWarnings("unchecked")
57  	public ILink getLink(boolean post, Object parameter) {
58          Map<String,String> parameters = (Map<String,String>) parameter;
59          parameters.put(ServiceConstants.SERVICE, getName());
60          
61          return _linkFactory.constructLink(this, false, parameters, true);
62  	}
63  	
64  	/*
65  	 * Our NoOSIVFForStreamServiceFilter prevents us from getting a
66  	 * DB-connection. This is good, because we would not want to have a
67  	 * connection for each stream. There can be many streams, but we only have
68  	 * so many parallel connections. Therefore, we do 'manual'
69  	 * connection-handling in this method.
70  	 *  
71  	 * (non-Javadoc)
72  	 * @see org.apache.tapestry.engine.IEngineService#service(org.apache.tapestry.IRequestCycle)
73  	 */
74  	public void service(IRequestCycle cycle) throws IOException {
75  		boolean outputMetaData = "1".equals(cycle.getInfrastructure().getRequest().getHeader("Icy-MetaData"));
76  		long userid = Long.parseLong(cycle.getParameter("userid"));
77  		String passhash = cycle.getParameter("passhash");
78  		
79  		final DJ dj = StreamMaster.getDJByUser(userid);
80  		if (dj==null) {
81  			//Someone tries to open a stream without logging on to the application: No music for you!
82  			log.error("User-ID: "+userid+" has not logged on, ignoring stream-request");
83  			return;
84  		}
85  		
86  		IUser user = dj.getUser();
87  		if (passhash==null || !passhash.equals(user.getPassword())) {
88  			log.debug("False credentials provided, ignoring stream-request.");
89  		} else {
90  			if (log.isDebugEnabled()) {
91  				log.debug("Stream started for: "+getServletRequest().getRemoteAddr());
92  				log.debug("Metadata requested: "+ (outputMetaData ? "yes" : "no"));
93  				log.debug("User ID: "+userid);
94  				log.debug("Metadata interval and buffer size: "+BUFFER_SIZE);
95  			}
96  			
97  			Song song = null;
98  			
99  	        try {
100 				ShoutcastOutputStream sout = new ShoutcastOutputStream(
101 								initOutputStream(outputMetaData),
102 								BUFFER_SIZE,
103 								outputMetaData
104 				);
105 				MpegOutputStream out = new MpegOutputStream(sout);
106 				
107 				boolean doStream = true;
108 				while (doStream) {
109 					song = chooseSong(dj);
110 					if (song!=null) {
111 						log.debug("Selected new song for User ID: "+userid);
112 						try {
113 							dj.setPlaying(true);
114 							long startTime = System.currentTimeMillis();
115 							
116 							sout.setMetadata(song.getBand().getName() +	" - " +	song.getName(),"");
117 							streamSong(out,song,dj);
118 							
119 							long endTime = System.currentTimeMillis();
120 							long songDuration = endTime - startTime;
121 							if(song.getLength()>songDuration*2) {
122 								log.error("The song played in less than half its real length. Synchronization lost.");
123 							} else {
124 								dj.confirmPlay();
125 							}
126 						} catch (SkipException ske) {
127 							dj.confirmSkip();
128 						} catch (MusiControllerException mce) {
129 							//Do nothing, just select a new file.
130 							//TODO When no file exists, this will cause MC to try all files in the database
131 							//TODO Maybe it is better to stop after 10 missing files, and to reset
132 							//TODO this counter after one succesful streamSong()-call
133 							log.error(mce.getMessage());
134 						} catch (SocketException se) {
135 							doStream = false;
136 							//TODO Mark song as played if user listened more than 70% of it
137 						}
138 					}
139 				}
140 	        } catch (Exception e) { //User aborted the stream(?)
141 				log.error(e);
142 			}
143 	
144 			log.debug("Stream ended for User ID: "+userid);	
145 			dj.setPlaying(false);
146 		}
147 	}
148 	
149 	/**
150 	 * Surrounds the song-choosing with a Session.
151 	 * @param dj The DJ.
152 	 * @return The chosen Song.
153 	 */
154 	private Song chooseSong(final DJ dj) {
155 		Song song = null;
156 		SessionFactory factory = dj.getMusiController().getDao().getSessionFactory2();
157 		Session session = factory.openSession();
158 	    TransactionSynchronizationManager.bindResource(factory, new SessionHolder(session));
159 		Transaction tx = session.beginTransaction();
160 		try {
161 			song=dj.choose();
162 			File tmp = song.getLink().getFile(); //Nasty method to initialize the otherwise lazy property
163 			log.debug(tmp.getAbsolutePath());
164 			tx.commit();
165 		} catch (HibernateException e) {
166 			log.error(e.getMessage());
167 			tx.rollback();
168 		} finally {
169 			session.close();
170 			TransactionSynchronizationManager.unbindResource(factory);
171 		}
172 		return song;
173 	}
174 	
175 	private OutputStream initOutputStream(boolean metadata) throws IOException {
176 		OutputStream out = _response.getOutputStream(new ContentType("audio/mpeg"));
177 		
178 		//Spit out Shoutcast-headers.
179 		_response.setHeader("icy-notice1","<BR>This stream requires <a href=\"http://www.winamp.com/\">Winamp</a><BR>");
180 		_response.setHeader("icy-notice1","MusiController SHOUTcast-implementation<BR>");
181 		_response.setHeader("icy-name","MusiController");
182 		_response.setHeader("icy-genre","All sorts");
183 		_response.setHeader("icy-url","http://musicontroller.sourceforge.net");
184 		_response.setHeader("icy-pub","0");
185 		if (metadata) _response.setIntHeader("icy-metaint",BUFFER_SIZE);
186 		_response.setHeader("icy-br","192");
187 		
188 		log.debug("Outputstream initialized");
189 		
190 		return out;
191 	}
192 
193 	private void streamSong(MpegOutputStream out, Song song, final DJ dj) throws IOException, SkipException, MusiControllerException {
194 		InputStream in = null;
195 		File songfile = song.getLink().getFile();
196 		try {
197 			in = new FileInputStream(songfile);
198 			out.reset();
199 			copy(in,out,
200 				new IStreamController() {
201 					public boolean mustSkip() {
202 						return dj.mustSkip();
203 					}
204 					
205 					public void setPlayingTime(int millis) {
206 						dj.setPlayingTime(millis);
207 					}
208 				}
209 			);
210 		} catch (FileNotFoundException e) {
211 			//ignore, so the next song will be played
212 			String s = "File not found! "+songfile.getAbsolutePath();
213 			log.fatal(s);
214 			throw new MusiControllerException(s);
215 		} finally {
216 			if (in!=null) {
217 				in.close();
218 			}
219 		}
220 	}
221 	
222 	public String getName() {
223 		return SERVICE_NAME;
224 	}
225 
226     public void setLinkFactory(LinkFactory linkFactory) {
227         _linkFactory = linkFactory;
228     }
229 
230     public void setResponse(WebResponse response) {
231         _response = response;
232     }	
233     
234     /**
235      * Copy method that can be interrupted by a DJ
236      * @param in The inputstream
237      * @param out The outputstream
238      * @param controller The controller that can interrupt the copy-process
239      * @throws IOException When something really goes wrong ;-)
240      * @throws SkipException When an interrupt occured
241      */
242 	public void copy(InputStream in, MpegOutputStream out, IStreamController controller) throws IOException, SkipException {
243 		log.debug("Streaming from inputstream...");
244 		
245 		int read;
246 		byte[] buf = new byte[16384];
247 
248 		while ((read=in.read(buf,0,buf.length))!=-1) {
249 			out.write(buf,0,read);
250 			if (controller.mustSkip()) {
251 				log.debug("Skiprequest received");
252 				throw new SkipException();
253 			}
254 			controller.setPlayingTime(out.getStreamedLengthInMillis());
255 		}
256 		log.debug("Streaming from inputstream done.");
257 	}
258 	
259 }