package com.pugh.sockso.web.action; import com.pugh.sockso.music.stream.MusicStream; import com.pugh.sockso.Constants; import com.pugh.sockso.Properties; import com.pugh.sockso.Utils; import com.pugh.sockso.db.Database; import com.pugh.sockso.music.Track; import com.pugh.sockso.music.encoders.BuiltinEncoder; import com.pugh.sockso.music.encoders.CustomEncoder; import com.pugh.sockso.music.encoders.Encoder; import com.pugh.sockso.music.encoders.Encoders; import static com.pugh.sockso.music.encoders.Encoders.Type.BUILTIN; import static com.pugh.sockso.music.encoders.Encoders.Type.CUSTOM; import com.pugh.sockso.music.stream.ChunkedStream; import com.pugh.sockso.music.stream.RangeStream; import com.pugh.sockso.music.stream.RangeStream.Range; import com.pugh.sockso.music.stream.SimpleStream; import com.pugh.sockso.web.BadRequestException; import com.pugh.sockso.web.Request; import com.pugh.sockso.web.Response; import com.pugh.sockso.web.User; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.log4j.Logger; public class Streamer extends BaseAction { private static final Logger log = Logger.getLogger( Streamer.class ); /** * handles a request * */ public void handleRequest() throws SQLException, IOException, BadRequestException { final Request req = getRequest(); final int trackId = Integer.parseInt( req.getUrlParam( 1 ) ); final Track track = Track.find( getDatabase(), trackId ); if ( track == null ) { throw new BadRequestException( "Invalid track ID", 404 ); } final MusicStream stream = getStream( track ); playTrack( track, stream ); } /** * if users are required to login then authentication will be done via session * information send in the playlists. * */ @Override public boolean requiresLogin() { final Properties p = getProperties(); return p.get( Constants.STREAM_REQUIRE_LOGIN ).equals( Properties.YES ); } protected MusicStream getStream( final Track track ) throws IOException, BadRequestException { MusicStream stream; // check if we're using an encoder first Encoder encoder = getEncoder( track ); if ( encoder != null ) { // use Chunked stream strategy stream = new ChunkedStream( track, encoder, getProperties() ); } else if ( hasRangeHeader() ) { // stream from range Range range = processRangeRequest(track); stream = new RangeStream( track, range ); } else { // stream normally (unaltered) stream = new SimpleStream( track ); } return stream; } /** * streams a particular track to the response object * * @param trackId the track id to playTrack * * @throws SQLException * @throws IOException * @throws BadRequestException * */ protected void playTrack( final Track track, final MusicStream stream ) throws SQLException, IOException, BadRequestException { logTrackPlayed( track ); final Response response = getResponse(); stream.setHeaders( response ); response.sendHeaders(); stream.sendAudioStream( new DataOutputStream(response.getOutputStream()) ); } protected Encoder getEncoder( final Track track ) throws IOException { final Properties p = getProperties(); final String ext = Utils.getExt(track.getPath()); final String type = p.get(Constants.PROP_ENCODERS_PREFIX + ext); if ( type.equals("") ) { return null; } Encoders.Type encoderType; try { encoderType = Encoders.Type.valueOf(type); } catch (IllegalArgumentException e) { log.error("Encoder type not found", e); return null; } switch (encoderType) { // 1. use a builtin encoder? case BUILTIN: final String name = p.get(Constants.PROP_ENCODERS_PREFIX + ext + ".name"); final BuiltinEncoder encoder = Encoders.getBuiltinEncoderByName(name).getEncoder(); if ( encoder != null ) { return encoder; } // 2. use a custom command to encode? case CUSTOM: final String command = p.get(Constants.PROP_ENCODERS_PREFIX + ext + ".command"); if ( !command.equals("") ) { return new CustomEncoder(command); } // 3. otherwise stream unaltered (no encoder) default: return null; } } /** * Check if the header "Range" exists in the HTTP Request * * @return */ private boolean hasRangeHeader() { final String rangeHeader = getRequest().getHeader("Range"); return !rangeHeader.isEmpty(); } /** * * Process range request headers to seek to positions in the audio stream * * GET /2390/2253727548_a413c88ab3_s.jpg HTTP/1.1 * Host: farm3.static.flickr.com * Range: bytes=1000- * * HTTP/1.0 206 Partial Content * * Content-Length: 2980 * Content-Range: bytes 1000-3979/3980 * */ protected Range processRangeRequest( final Track track ) throws IOException, BadRequestException { final String rangeHeader = getRequest().getHeader("Range"); final Pattern pattern = Pattern.compile("bytes=(\\d+)-(\\d+)?"); final Matcher matcher = pattern.matcher(rangeHeader); if ( !matcher.matches() ) { log.error("Bad \"Range\" header: " + rangeHeader); throw new BadRequestException("Invalid range", 416); } try { long beginPos = Long.parseLong(matcher.group(1)); long endPos = -1; String endMatch = matcher.group(2); if ( endMatch != null ) { endPos = Long.parseLong(endMatch); } final long trackLength = new File(track.getPath()).length(); if ( endPos < 0 ) { endPos = trackLength - 1; } if ( beginPos < 0 || beginPos >= trackLength || endPos >= trackLength || endPos <= beginPos ) { log.error("Bad \"Range\" values: " + beginPos + "-" + endPos); throw new BadRequestException("Invalid range", 416); } return new Range(beginPos, endPos); } catch (NumberFormatException e) { log.error("Bad \"Range\" header", e); throw new BadRequestException("Invalid range", 416); } } /** * @TODO Create a dao class for PlayLog * * logs the fact that a track has been played * * @param trackId the track id * @param path the path to the file * */ protected void logTrackPlayed( final Track track ) { log.debug( "Track: " + track.getPath() ); PreparedStatement st = null; try { final Database db = getDatabase(); final User user = getUser(); final String sql = " insert into play_log ( track_id, date_played, user_id ) " + " values ( ?, current_timestamp, ? ) "; st = db.prepare( sql ); st.setInt( 1, track.getId() ); if ( user != null ) st.setInt( 2, user.getId() ); else st.setNull( 2, Types.INTEGER ); st.execute(); } catch ( SQLException e ) { log.error( e ); } finally { Utils.close( st ); } } }