Basic functionallity

This commit is contained in:
erik.bystrom
2008-07-14 18:04:53 +00:00
commit 6e8c389cbe
69 changed files with 4206 additions and 0 deletions

30
hextool.py Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/python
import sys
def toBinary(value):
s = ""
v = 128
for i in range(8):
if value & v:
s += "1"
else:
s += "0"
v /= 2
return s
binstr = ""
for n in range(1, len(sys.argv)):
try:
value = int(sys.argv[n])
except ValueError:
value = int(sys.argv[n], 16)
bin = toBinary(value)
print "%3d = 0x%02x = %sb" % (value, value, bin),
if (31 < value and value < 255):
print " = %c" % value
else:
print ""
binstr += bin + " "
print binstr

186
pom.xml Executable file
View File

@@ -0,0 +1,186 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>se.slackers.locality</groupId>
<artifactId>locality</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>locality</name>
<description>locality</description>
<url>http://slackers.se/app/locality</url>
<!-- DEVELOPERS -->
<developers>
<developer>
<id>eb</id>
<name>
Erik Bystr&ouml;m
</name>
<email>erik.bystrom+lastfmsaver@gmail.com</email>
<roles>
<role>Developer</role>
</roles>
<organization>slackers.se</organization>
<timezone>+1</timezone>
</developer>
</developers>
<!-- DEPENDENCIES -->
<dependencies>
<!-- SPRING -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5</version>
</dependency>
<!-- DATABASE -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.0.67</version>
</dependency>
<!-- HIBERNATE -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>3.2.6.ga</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-commons-annotations</artifactId>
<version>3.3.0.ga</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>3.3.0.ga</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-hibernate3</artifactId>
<version>2.0.8</version>
</dependency>
<!-- COMMONS -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>commons-digester</groupId>
<artifactId>commons-digester</artifactId>
<version>1.8</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.2.2</version>
</dependency>
<!-- JDOM -->
<dependency>
<groupId>jdom</groupId>
<artifactId>jdom</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.1</version>
</dependency>
<!-- JGoodies -->
<dependency>
<groupId>jgoodies</groupId>
<artifactId>forms</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>org.swinglabs</groupId>
<artifactId>swingx</artifactId>
<version>0.9</version>
</dependency>
<!-- Http Client -->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<!-- TESTING -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>2.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jxr-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<links>
<link>
http://java.sun.com/j2ee/1.4/docs/api
</link>
<link>
http://java.sun.com/j2se/1.5.0/docs/api
</link>
</links>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>
maven-project-info-reports-plugin
</artifactId>
</plugin>
</plugins>
</reporting>
</project>

View File

@@ -0,0 +1,20 @@
package se.slackers.jss.mediastream;
import java.io.InputStream;
/**
* BaseClass for classes that stream audio data.
* @author bysse
*
*/
abstract public class MediaStream {
private InputStream inputStream;
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
}

View File

@@ -0,0 +1,48 @@
package se.slackers.locality.dao;
import java.util.List;
import se.slackers.locality.model.MetaTag;
public interface MetaTagDao {
/**
* Removes the metatag from the database and deletes all references to it.
* @param tag
*/
public void delete(MetaTag tag);
/**
* Find a metatag by the metatag id. If no metatag was found a DataRetrievalFailureException is thrown.
*
* @param id
* @return
*/
public MetaTag get(Long id);
/**
* Find a metatag by the name of the metatag. If no metatag was found a DataRetrievalFailureException is thrown.
*
* @param name
* @return
*/
public MetaTag get(String name);
/**
* Searches for metatags that have similar names to the given string. Before the search the name is converted to
* lower case.
*
* @param name
* @return
*/
public List<MetaTag> getLike(String name);
/**
* Saves or updates the metatag.
*
* @param metatag
*/
public void save(MetaTag metatag);
}

View File

@@ -0,0 +1,45 @@
package se.slackers.locality.dao;
import java.util.List;
import se.slackers.locality.model.Tag;
public interface TagDao {
/**
* Removes the tag from the database and deletes all references to it.
* @param tag
*/
public void delete(Tag tag);
/**
* Find a tag by the tag id. If no tag was found a DataRetrievalFailureException is thrown.
*
* @param id
* @return
*/
public Tag get(Long id);
/**
* Find a tag by the name of the tag. If no tag was found a DataRetrievalFailureException is thrown.
*
* @param name
* @return
*/
public Tag get(String name);
/**
* Searches for tags that have similar names to the given string. Before the search the name is converted to
* lower case.
*
* @param name
* @return
*/
public List<Tag> getLike(String name);
/**
* Saves or updates the tag.
*
* @param tag
*/
public void save(Tag tag);
}

View File

@@ -0,0 +1,64 @@
package se.slackers.locality.dao.hibernate;
import java.util.List;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import se.slackers.locality.dao.MetaTagDao;
import se.slackers.locality.model.MetaTag;
public class MetaTagDaoImpl extends HibernateDaoSupport implements MetaTagDao {
/**
* {@inheritDoc}
*/
public void delete(MetaTag tag) {
getHibernateTemplate().delete(tag);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
public MetaTag get(Long id) {
List<MetaTag> result = (List<MetaTag>)getHibernateTemplate().find("from MetaTag tag fetch all properties where tag.id=?", id);
if (result.isEmpty())
throw new DataRetrievalFailureException("No metatag with id "+id+" could be found");
assert result.size() == 1 : "More than one metatag found with id "+id;
return result.get(0);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
public MetaTag get(String name) {
List<MetaTag> result = (List<MetaTag>)getHibernateTemplate().find("from MetaTag tag fetch all properties where tag.name=?", name);
if (result.isEmpty())
throw new DataRetrievalFailureException("No metatag with name "+name+" could be found");
assert result.size() == 1 : "More than one metatag found with name "+name;
return result.get(0);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
public List<MetaTag> getLike(String name) {
return (List<MetaTag>)getHibernateTemplate().find("from MetaTag tag fetch all properties where lower(tag.name) like ? order by tag.name", name.toLowerCase());
}
/**
* {@inheritDoc}
*/
public void save(MetaTag tag) {
getHibernateTemplate().saveOrUpdate(tag);
}
}

View File

@@ -0,0 +1,64 @@
package se.slackers.locality.dao.hibernate;
import java.util.List;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import se.slackers.locality.dao.TagDao;
import se.slackers.locality.model.Tag;
public class TagDaoImpl extends HibernateDaoSupport implements TagDao {
/**
* {@inheritDoc}
*/
public void delete(Tag tag) {
getHibernateTemplate().delete(tag);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
public Tag get(Long id) {
List<Tag> result = (List<Tag>)getHibernateTemplate().find("from Tag tag fetch all properties where tag.id=?", id);
if (result.isEmpty())
throw new DataRetrievalFailureException("No tag with id "+id+" could be found");
assert result.size() == 1 : "More than one tag found with id "+id;
return result.get(0);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
public Tag get(String name) {
List<Tag> result = (List<Tag>)getHibernateTemplate().find("from Tag tag fetch all properties where tag.name=?", name);
if (result.isEmpty())
throw new DataRetrievalFailureException("No tag with name "+name+" could be found");
assert result.size() == 1 : "More than one tag found with name "+name;
return result.get(0);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
public List<Tag> getLike(String name) {
return (List<Tag>)getHibernateTemplate().find("from Tag tag fetch all properties where lower(tag.name) like ? order by tag.name", name.toLowerCase());
}
/**
* {@inheritDoc}
*/
public void save(Tag tag) {
getHibernateTemplate().saveOrUpdate(tag);
}
}

View File

@@ -0,0 +1,115 @@
package se.slackers.locality.data;
import java.nio.ByteBuffer;
public class CircularBuffer {
private ByteBuffer buffer;
private int readIndex = 0;
private int writeIndex = 0;
public CircularBuffer(int bufferSize) {
buffer = ByteBuffer.allocateDirect(bufferSize);
}
public void reset() {
readIndex = 0;
writeIndex = 0;
}
public int read(byte [] dest, int offset, int length) {
assert length < buffer.capacity() : "The requested read is bigger than the buffer";
if (writeIndex == readIndex) {
return 0;
}
buffer.position(readIndex);
if (writeIndex < readIndex) {
int remainder = buffer.remaining();
if (remainder < length) {
buffer.get(dest, offset, remainder);
offset += remainder;
length -= remainder;
readIndex = 0;
buffer.position(readIndex);
int space = writeIndex-readIndex;
if (space <= length) {
length = space;
}
buffer.get(dest, offset, length);
readIndex += length;
return remainder + length;
} else {
buffer.get(dest, offset, remainder);
readIndex += remainder;
return remainder;
}
} else {
int space = writeIndex - readIndex;
if (space <= length) {
length = space;
}
buffer.get(dest, offset, length);
readIndex += length;
return length;
}
}
public boolean write(byte [] source, int offset, int length) {
assert length < buffer.capacity() : "The requested write is bigger than the buffer";
buffer.position(writeIndex);
if ( (readIndex <= writeIndex && writeIndex + length < buffer.capacity()) ||
(writeIndex < readIndex && length < readIndex-writeIndex)) {
// source fits in the remainder of the buffer
buffer.put(source, offset, length);
writeIndex += length;
return true;
} else {
// the source don't fit in the buffer without wrapping
int remainder = buffer.remaining();
if (readIndex < writeIndex && length > readIndex + remainder) {
return false;
}
if (writeIndex < readIndex && length > readIndex-writeIndex) {
return false;
}
buffer.put(source, offset, remainder);
offset += remainder;
length -= remainder;
writeIndex = 0;
buffer.position(writeIndex);
assert length < readIndex : "There is not enough room for this write operation";
buffer.put(source, offset, length);
writeIndex += length;
return true;
}
}
public boolean isEmpty() {
return writeIndex == readIndex;
}
public boolean isFull() {
if (writeIndex+1 <= buffer.capacity() && writeIndex+1 == readIndex)
return true;
if (writeIndex == buffer.capacity()-1 && readIndex == 0)
return true;
return false;
}
}

View File

@@ -0,0 +1,158 @@
package se.slackers.locality.data;
import java.nio.ByteBuffer;
import se.slackers.locality.exception.InvalidBufferPositionException;
public class ExpandOnWriteCircularBuffer {
private ByteBuffer buffer;
private int readOffset = 0;
private int readIndex = 0;
private int writeIndex = 0;
public ExpandOnWriteCircularBuffer(int bufferSize) {
buffer = ByteBuffer.allocateDirect(bufferSize);
}
public void reset() {
readOffset = 0;
readIndex = 0;
writeIndex = 0;
}
public synchronized int read(int position, byte [] dest, int destOffset, int destLength) {
assert destLength < buffer.capacity() : "The requested read is bigger than the buffer";
// make sure the position is larger than the smallest buffer position
if (position < readOffset) {
throw new InvalidBufferPositionException("Read position "+position+" is smaller than ["+readOffset+"]");
}
// make sure the position is smaller then the smallest buffer position
int offset = position - readOffset;
if (offset > getBytesInBuffer()) {
throw new InvalidBufferPositionException("Read position "+position+" is larger then ["+(readOffset+getBytesInBuffer())+"]");
}
// check if the buffer is empty
if (writeIndex == readIndex) {
return 0;
}
buffer.position(offset);
if (writeIndex < readIndex) {
int remainder = buffer.remaining();
if (remainder < destLength) {
buffer.get(dest, destOffset, remainder);
destOffset += remainder;
destLength -= remainder;
buffer.position(0);
int space = writeIndex-0;
if (space <= destLength) {
destLength = space;
}
buffer.get(dest, destOffset, destLength);
return remainder + destLength;
} else {
buffer.get(dest, destOffset, remainder);
return remainder;
}
} else {
int space = writeIndex - offset;
if (space <= destLength) {
destLength = space;
}
buffer.get(dest, destOffset, destLength);
return destLength;
}
}
public synchronized boolean write(byte [] source, int offset, int length) {
assert length < buffer.capacity() : "The requested write is bigger than the buffer";
buffer.position(writeIndex);
if (length < buffer.capacity()-getBytesInBuffer()) {
// the write fits in the buffer without changing the readIndex
if (writeIndex <= readIndex) {
assert (readIndex-writeIndex) == (buffer.capacity()-getBytesInBuffer()) : "Buffer size is invalid";
buffer.put(source, offset, length);
writeIndex += length;
return true;
} else {
int remainder = buffer.remaining();
if (length < remainder) {
// the write fits in the remaining buffer
buffer.put(source, offset, length);
writeIndex += length;
return true;
} else {
// the write needs to be wrapped
buffer.put(source, offset, remainder);
buffer.position(0);
buffer.put(source, offset+remainder, length-remainder);
writeIndex = length-remainder;
return true;
}
}
} else {
// readIndex needs to be changed after the write
int remainder = buffer.remaining();
if (length < remainder) {
// the write fits in the remaining buffer
buffer.put(source, offset, length);
writeIndex += length;
} else {
// the write needs to be wrapped
buffer.put(source, offset, remainder);
buffer.position(0);
buffer.put(source, offset+remainder, length-remainder);
writeIndex = length-remainder;
}
int oldRead = readIndex;
readIndex = writeIndex + 1;
if (readIndex >= buffer.capacity()) {
readIndex -= buffer.capacity();
}
// increase the offset index
if (readIndex < oldRead) {
readOffset += buffer.capacity()-oldRead + readIndex;
} else {
readOffset += readIndex-oldRead;
}
return true;
}
}
public int getReadOffset() {
return readOffset;
}
public boolean isEmpty() {
return writeIndex == readIndex;
}
public boolean isFull() {
if (writeIndex+1 <= buffer.capacity() && writeIndex+1 == readIndex)
return true;
if (writeIndex == buffer.capacity()-1 && readIndex == 0)
return true;
return false;
}
private int getBytesInBuffer() {
if (writeIndex < readIndex) {
return (buffer.capacity()-readIndex) + writeIndex;
}
return writeIndex-readIndex;
}
}

View File

@@ -0,0 +1,115 @@
package se.slackers.locality.data;
import java.util.Iterator;
import java.util.LinkedList;
import org.apache.log4j.Logger;
import se.slackers.locality.exception.FrameHasNotBeenLoadedException;
import se.slackers.locality.exception.FrameIsTooOldException;
import se.slackers.locality.exception.FrameStorageIsEmptyException;
/**
* Threadsafe.
*
* @author bysse
*
*/
public class FixedFrameSizeFrameStorage implements FrameStorage {
private static final Logger log = Logger.getLogger(FixedFrameSizeFrameStorage.class);
private LinkedList<FrameStorageEntry> frames = new LinkedList<FrameStorageEntry>();
private long frameLength = 26; // MP3 frame length
/**
* {@inheritDoc}
*/
public synchronized FrameStorageEntry find(long time) {
if (frames.isEmpty()) {
throw new FrameStorageIsEmptyException();
}
long firstFrameTime = frames.getFirst().getStartTime();
long lastFrameTime = frames.getLast().getStopTime();
// make sure the frame is within the represented interval
if (lastFrameTime <= time) {
//log.debug("Request: "+time+", LastFrame: "+lastFrameTime+", Diff: "+(time-lastFrameTime));
throw new FrameHasNotBeenLoadedException();
}
if (time < firstFrameTime) {
throw new FrameIsTooOldException();
}
int index = (int) ((time - firstFrameTime) / frameLength);
return frames.get(index);
}
/**
* {@inheritDoc}
*/
public synchronized void add(FrameStorageEntry entry) {
frames.add(entry);
}
/**
* {@inheritDoc}
*/
public synchronized void purgeUntil(long time) {
//log.debug("Purging framestorage until "+time);
Iterator<FrameStorageEntry> iterator = frames.iterator();
while (iterator.hasNext()) {
if (iterator.next().getStopTime() <= time) {
iterator.remove();
} else {
break;
}
}
}
/**
* {@inheritDoc}
*/
public synchronized long getFirstFrameTime() {
if (frames.isEmpty()) {
throw new FrameStorageIsEmptyException();
}
return frames.getFirst().getStartTime();
}
/**
* {@inheritDoc}
*/
public synchronized long getLastFrameTime() {
if (frames.isEmpty()) {
throw new FrameStorageIsEmptyException();
}
return frames.getLast().getStopTime();
}
/**
* Returns the frame length that is used by the instance.
* @return The frame length in milliseconds
*/
public long getFrameLength() {
return frameLength;
}
public void setFrameLength(long frameLength) {
this.frameLength = frameLength;
}
/**
* {@inheritDoc}
*/
public synchronized void clear() {
log.debug("Clearing frame storage");
frames.clear();
}
}

View File

@@ -0,0 +1,54 @@
package se.slackers.locality.data;
import se.slackers.locality.exception.FrameHasNotBeenLoadedException;
public interface FrameStorage {
/**
* Adds a frame to the FrameStorage. This method only adds the frame to the
* end of the storage. So adding out-of-order frames will cause error in
* playback.
*
* @param entry
*/
public void add(FrameStorageEntry entry);
/**
* Returns the frame that overlaps the given time. If the FrameStorage is
* empty {@link FrameStorageIsEmptyException} is be thrown. If no frame
* could be found for the specified time {@link FrameHasNotBeenLoadedException} or
* {@link FrameIsTooOldException} is thrown.
*
* @param time
* @return A FrameStorageEntry that overlapped the given time.
*/
public FrameStorageEntry find(long time);
/**
* Removes all frames that doesn't overlap the given time.
*
* @param time
*/
public void purgeUntil(long time);
/**
* Clears the frame storage.
*/
public void clear();
/**
* Returns the start time of the first frame. If the storage is empty
* {@link FrameStorageIsEmptyException} will be thrown.
*
* @return Start time of first frame.
*/
public long getFirstFrameTime();
/**
* Returns the end time of the last frame. If the storage is empty
* {@link FrameStorageIsEmptyException} will be thrown.
*
* @return End time of the last frame in storage.
*/
public long getLastFrameTime();
}

View File

@@ -0,0 +1,50 @@
package se.slackers.locality.data;
import se.slackers.locality.media.Frame;
/**
* Immutable class that wraps a frame with start and stop times.
* @author bysse
*
*/
public class FrameStorageEntry implements Comparable<FrameStorageEntry> {
private long startTime;
private long stopTime;
private Frame frame;
public FrameStorageEntry(long time, Frame frame) {
this.startTime = time;
this.stopTime = time + frame.getLength();
this.frame = frame;
}
public long getStartTime() {
return startTime;
}
public long getStopTime() {
return stopTime;
}
public Frame getFrame() {
return frame;
}
/**
* This implementation considers overlapping intervals to be equal.
*/
public int compareTo(FrameStorageEntry o) {
if (stopTime < o.startTime)
return -1;
if (startTime >= o.stopTime)
return 1;
return 0;
}
@Override
public String toString() {
return "Spans from "+getStartTime()+" to "+getStopTime() +" ("+getFrame()+")";
}
}

View File

@@ -0,0 +1,180 @@
package se.slackers.locality.data;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
import org.springframework.util.StringUtils;
import se.slackers.locality.media.queue.MediaQueue;
import se.slackers.locality.media.queue.MediaQueueProcessorListener;
import se.slackers.locality.model.Media;
import se.slackers.locality.model.Metadata;
import se.slackers.locality.model.MetadataType;
/**
* Controls when a full metadata chunk should be rendered and in which format.
* @author bysse
*
*/
public class MetadataManager implements MediaQueueProcessorListener {
private static Logger log = Logger.getLogger(MetadataManager.class);
private final static int maximumMetadataLength = 4095;
private final static Pattern field = Pattern.compile("(\\$\\{([^\\}\\$]+)\\})|(\\$([^\\s\\?\\$]+))");
private final static Pattern condition = Pattern.compile("\\?\\(([^,]+),([^\\)]+)\\)");
private String format = "$artist ?(album,- )$album ?(title,- )$title";
private String cachedMetadataString = "";
private Metadata currentMetadata = null;
private long sendMetadataInterval = 15000;
private long lastMetadataChunk = 0;
/**
* Default constructor. Sets the metadata to "Nothing playing"
*/
public MetadataManager() {
setMetadata(Metadata.create("Nothing playing", null, null));
}
/**
* Formats and returns a byte array containing metadata information from the MediaQueue.
* @param mediaQueue
* @return
*/
public byte [] getMetaData(MediaQueue mediaQueue) {
long time = System.currentTimeMillis();
if (time - lastMetadataChunk > sendMetadataInterval) {
log.debug("Send full metadata chunk ("+cachedMetadataString+")");
//.. return a full metadata string
lastMetadataChunk = time;
// restrict the length of the metadata
if (cachedMetadataString.length() > maximumMetadataLength ) {
cachedMetadataString = cachedMetadataString.substring(0, maximumMetadataLength);
}
int metadataLenth = cachedMetadataString.length();
int encodedLength = ((int)Math.ceil(metadataLenth / 16.0));
int blockLength = 16 * encodedLength;
byte [] result = new byte[blockLength+1];
result[0] = (byte)encodedLength;
System.arraycopy(cachedMetadataString.getBytes(), 0, result, 1, metadataLenth);
// add padding to the block
for (int i=metadataLenth+1;i<blockLength;i++) {
result[i] = 0;
}
return result;
} else {
log.debug("Sending zero length metadata chunk");
// return a zero length metadata chunk
return new byte[] {0};
}
}
/**
* Renders the metadata string sent to the client.
* @param format
* @param info
* @return
*/
protected String parseFormat(String format, Metadata info) {
String result = format;
//.. replace all fields in the format string
Matcher fieldmatch = field.matcher(format);
while (fieldmatch.find()) {
String fieldname = fieldmatch.group(2);
if (fieldname == null) {
fieldname = fieldmatch.group(4);
}
if (fieldname == null) {
log.warn("Metadata format uses invalid field syntax '"+fieldmatch.group(0)+"'");
continue;
}
try {
MetadataType type = MetadataType.valueOf(fieldname.toUpperCase());
// if there is metadata for this field, insert the data into the string
// otherwise just remove the expression from the string
if ( StringUtils.hasText( info.get(type) ) ) {
result = result.replace(fieldmatch.group(0), info.get(type));
} else {
result = result.replace(fieldmatch.group(0), "");
}
} catch (IllegalArgumentException e) {
log.warn("Metadata format uses invalid field name '"+fieldname+"' in expression '"+fieldmatch.group(0)+"'");
}
}
//.. replace all conditionals in the format string
Matcher conditionmatch = condition.matcher(format);
while (conditionmatch.find()) {
String fieldname = conditionmatch.group(1);
String text = conditionmatch.group(2);
if (fieldname == null) {
log.warn("Metadata format uses invalid field syntax '"+fieldmatch.group(0)+"'");
continue;
}
try {
MetadataType type = MetadataType.valueOf(fieldname.toUpperCase());
if ( StringUtils.hasText( info.get(type) ) ) {
result = result.replace(conditionmatch.group(0), text);
} else {
result = result.replace(conditionmatch.group(0), "");
}
} catch (IllegalArgumentException e) {
log.warn("Metadata format uses invalid field name '"+fieldname+"' in expression '"+conditionmatch.group(0)+"'");
}
}
return result;
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
// call setMetadata to rerender the metadata string
setMetadata(currentMetadata);
}
/**
* {@inheritDoc}
*/
public void nextMedia(Media media, Metadata metadata) {
log.debug("nextMedia event triggered");
setMetadata(metadata);
// trigger a full metadata chunk
lastMetadataChunk = 0;
}
/**
* Sets and renders new metadata with the current format.
* @param metadata
*/
public void setMetadata(Metadata metadata) {
log.debug("New metadata set ["+metadata+"] Thread: "+Thread.currentThread());
currentMetadata = metadata;
cachedMetadataString = parseFormat("StreamTitle='"+getFormat(), currentMetadata);
}
}

View File

@@ -0,0 +1,90 @@
package se.slackers.locality.data;
public class TimeOffsetBuffer {
private int [] offset;
private long [] time;
private int bufferSize;
private int readIndex;
private int writeIndex;
public TimeOffsetBuffer(int bufferSize) {
this.bufferSize = bufferSize;
this.offset = new int[bufferSize];
this.time = new long[bufferSize];
readIndex = 0;
writeIndex = 0;
}
public synchronized void write(int offset, int time) {
this.offset[writeIndex] = offset;
this.time[writeIndex] = time;
increaseWriteIndex();
}
/**
* Get the closest offset for the given time.
* @param time
* @return -1 if the time is larger than any time in the buffer
*/
public synchronized int getOffset(long time) {
for (int i=1;i<getBufferContentSize();i++) {
int index = (readIndex + i) % bufferSize;
if (this.time[index] > time) {
if (index > 0) {
return this.offset[index-1];
} else {
return this.offset[bufferSize-1];
}
}
}
return -1;
}
public long getMinTime() {
return this.time[readIndex];
}
public long getMaxTime() {
if (writeIndex > 0) {
return this.offset[writeIndex-1];
} else {
return this.offset[bufferSize-1];
}
}
/**
* Returns the number of elements in the buffer.
* @return
*/
private int getBufferContentSize() {
if (readIndex <= writeIndex) {
return writeIndex-readIndex;
}
return bufferSize-readIndex + writeIndex;
}
/**
* Increases the write index by one. This method also adjusts the
* readIndex.
*/
private void increaseWriteIndex() {
writeIndex++;
if (writeIndex == readIndex) {
readIndex++;
}
if (writeIndex >= bufferSize) {
writeIndex -= bufferSize;
}
if (readIndex >= bufferSize) {
readIndex -= bufferSize;
}
}
}

View File

@@ -0,0 +1,25 @@
package se.slackers.locality.exception;
public class CantCreateMediaReaderException extends RuntimeException {
public CantCreateMediaReaderException() {
super();
// TODO Auto-generated constructor stub
}
public CantCreateMediaReaderException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public CantCreateMediaReaderException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public CantCreateMediaReaderException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}

View File

@@ -0,0 +1,25 @@
package se.slackers.locality.exception;
/**
* Exception class for duplicate item errors in search tree insertions.
*
* @author Mark Allen Weiss
*/
public class DuplicateItemException extends RuntimeException {
/**
* Construct this exception object.
*/
public DuplicateItemException() {
super();
}
/**
* Construct this exception object.
*
* @param message
* the error message.
*/
public DuplicateItemException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,20 @@
package se.slackers.locality.exception;
/**
* Encapsules an exception in a RuntimeException.
* @author bysse
*
*/
public class EncapsuledExceptionRuntimException extends RuntimeException {
private static final long serialVersionUID = 794221656425404393L;
private Exception exception = null;
public EncapsuledExceptionRuntimException(Exception exception) {
this.exception = exception;
}
public void rethrow() throws Exception {
throw this.exception;
}
}

View File

@@ -0,0 +1,30 @@
package se.slackers.locality.exception;
public class FrameHasNotBeenLoadedException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 8666136563915734945L;
public FrameHasNotBeenLoadedException() {
super();
// TODO Auto-generated constructor stub
}
public FrameHasNotBeenLoadedException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public FrameHasNotBeenLoadedException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public FrameHasNotBeenLoadedException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}

View File

@@ -0,0 +1,30 @@
package se.slackers.locality.exception;
public class FrameIsTooOldException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = -3486181476553301458L;
public FrameIsTooOldException() {
super();
// TODO Auto-generated constructor stub
}
public FrameIsTooOldException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public FrameIsTooOldException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public FrameIsTooOldException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}

View File

@@ -0,0 +1,26 @@
package se.slackers.locality.exception;
public class FrameStorageIsEmptyException extends RuntimeException {
private static final long serialVersionUID = -5393933470236337451L;
public FrameStorageIsEmptyException() {
super();
// TODO Auto-generated constructor stub
}
public FrameStorageIsEmptyException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public FrameStorageIsEmptyException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public FrameStorageIsEmptyException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}

View File

@@ -0,0 +1,30 @@
package se.slackers.locality.exception;
public class IllegalRequestException extends Exception {
/**
*
*/
private static final long serialVersionUID = -8418140559118657718L;
public IllegalRequestException() {
super();
// TODO Auto-generated constructor stub
}
public IllegalRequestException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public IllegalRequestException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public IllegalRequestException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}

View File

@@ -0,0 +1,25 @@
package se.slackers.locality.exception;
public class InvalidBufferPositionException extends RuntimeException {
public InvalidBufferPositionException() {
super();
// TODO Auto-generated constructor stub
}
public InvalidBufferPositionException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public InvalidBufferPositionException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public InvalidBufferPositionException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}

View File

@@ -0,0 +1,88 @@
package se.slackers.locality.media;
public class Frame {
/**
* Frame length in ms
*/
private long length;
/**
* Frame size in bytes
*/
private int size;
/**
* Data buffer.
*/
private byte [] data;
public Frame() {
length = 0;
size = 0;
data = null;
}
public Frame(int allocationSize) {
length = 0;
size = 0;
data = new byte[allocationSize];
}
/**
* Makes a deep copy of the given frame.
* @param frame
*/
public Frame(Frame frame) {
length = frame.length;
size = frame.size;
data = new byte[size];
System.arraycopy(frame.data, 0, data, 0, size);
}
/**
* Get the length of this frame in milliseconds
* @return Length of frame in Ms
*/
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
/**
* Get the size of the frame in bytes.
* @return Size of frame in bytes
*/
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
/**
* Returns a reference to the data
* @return
*/
public byte[] getData() {
return data;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append(getLength());
str.append(" ms [");
str.append(getSize());
str.append(" bytes]");
return str.toString();
}
}

View File

@@ -0,0 +1,254 @@
package se.slackers.locality.media.queue;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Semaphore;
import org.apache.log4j.Logger;
import se.slackers.locality.exception.EncapsuledExceptionRuntimException;
import se.slackers.locality.media.Frame;
import se.slackers.locality.media.reader.MediaReader;
import se.slackers.locality.media.reader.MediaReaderFactory;
import se.slackers.locality.media.reader.SilentMediaReader;
import se.slackers.locality.model.Media;
import se.slackers.locality.model.Metadata;
public abstract class AbstractMediaQueueProcessor implements MediaQueueProcessor, Runnable {
private static final Logger log = Logger.getLogger(AbstractMediaQueueProcessor.class);
private MediaQueue mediaQueue;
private MediaReader mediaReader = null;
private MediaReader silentReader = new SilentMediaReader();
private MediaReaderFactory mediaReaderFactory = null;
private List<WeakReference<MediaQueueProcessorListener>> mediaQueueProcessorListeners = new ArrayList<WeakReference<MediaQueueProcessorListener>>();
private boolean stopProcessing;
private int activeClients = 0;
private Object stopProcessingMonitor = new Object();
private Semaphore initDeinit = new Semaphore(1);
/**
* {@inheritDoc}
*/
public synchronized void init() {
log.info("Acquire permit");
initDeinit.acquireUninterruptibly();
activeClients = 0;
stopProcessing = false;
checkMediaReader();
}
/**
* {@inheritDoc}
*/
public synchronized void deinit() {
mediaQueue.getFrameStorage().clear();
if (mediaReader != null) {
try {
mediaReader.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
mediaReader = null;
log.info("Releasing permit");
initDeinit.release();
}
/**
*
*/
public void run() {
Frame frame = new Frame(3000); // Maximum frame size for an mp3 is 2881
try {
while (stopProcessing == false) {
// If nothing is playing and there is nothing in the queue, exit the thread
if (mediaReader == null && mediaQueue.size() == 0) {
break;
}
checkMediaReader();
// .. read a frame and store it in the buffer
if (mediaReader != null) {
synchronized (this) {
readData(mediaReader, frame);
}
}
}
// TODO: Fix the error handling
} catch (RuntimeException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
// Notify all waiting threads
if (stopProcessing) {
synchronized(stopProcessingMonitor) {
stopProcessingMonitor.notifyAll();
}
}
}
/**
* Makes sure that there is a valid {@link MediaReader} instantiated as long as there are more entries in the queue.
*/
private void checkMediaReader() {
if (mediaReader == null && mediaQueue.size() > 0) {
// create a new mediaReader for the next media file in queue.
mediaReader = mediaReaderFactory.getMediaReader(mediaQueue.get(0));
try {
mediaReader.open(mediaQueue.get(0));
fireNextMediaEvent(mediaQueue.get(0), mediaReader.getMetadata());
} catch (IOException e) {
log.error("Can't open [" + mediaQueue.get(0) + "], skipping file");
mediaQueue.remove(0);
}
}
if (mediaReader != null && mediaReader.eof()) {
try {
mediaReader.close();
mediaReader = null;
// consume the played file
mediaQueue.remove(0);
if (mediaQueue.size() > 0) {
// create a new mediaReader for the next media file in
// queue.
mediaReader = mediaReaderFactory.getMediaReader(mediaQueue.get(0));
mediaReader.open(mediaQueue.get(0));
fireNextMediaEvent(mediaQueue.get(0), mediaReader.getMetadata());
} else {
mediaReader = silentReader;
}
} catch (IOException e) {
log.error("Can't open [" + mediaQueue.get(0) + "], skipping file");
mediaQueue.remove(0);
}
}
}
/**
* Reads a frame from the media and stores it in the FrameStorage.
*
* @param reader
* @param frame
* @return
*/
abstract protected void readData(MediaReader reader, Frame frame);
/**
* Stops the processing and waits until it really has stopped.
*/
public void stopProcessing() {
this.stopProcessing = true;
try {
synchronized(stopProcessingMonitor) {
stopProcessingMonitor.wait();
}
} catch (InterruptedException e) {
throw new EncapsuledExceptionRuntimException(e);
}
deinit();
mediaQueue.stopProcessor();
}
/**
* Returns the mediaQueue that is used by the processor
*/
public MediaQueue getMediaQueue() {
return mediaQueue;
}
/**
* Sets the media queue to be used by the processor. This method also ensures that the reverse dependency is
* correct.
*/
public void setMediaQueue(MediaQueue mediaQueue) {
this.mediaQueue = mediaQueue;
if (this.mediaQueue.getMediaQueueProcessor() != this) {
this.mediaQueue.setMediaQueueProcessor(this);
}
}
public MediaReaderFactory getMediaReaderFactory() {
return mediaReaderFactory;
}
public void setMediaReaderFactory(MediaReaderFactory mediaReaderFactory) {
this.mediaReaderFactory = mediaReaderFactory;
}
public void clientStartStreaming() {
activeClients++;
log.debug("Client connected, client="+activeClients);
// Update metadata for the connected client
/*
if (mediaQueue != null && mediaReader != null) {
fireNextMediaEvent(mediaQueue.get(0), mediaReader.getMetadata());
}
*/
}
public void clientStopsStreaming() {
activeClients--;
log.debug("Client disconnected, client="+activeClients);
if (activeClients == 0) {
// No clients listening, stop the stream
stopProcessing();
}
}
public void addMediaQueueProcessorListener(MediaQueueProcessorListener listener) {
log.debug("New listener added by thread "+Thread.currentThread());
mediaQueueProcessorListeners.add(new WeakReference<MediaQueueProcessorListener>(listener));
}
public void removeMediaQueueProcessorListener(MediaQueueProcessorListener listener) {
mediaQueueProcessorListeners.remove(listener);
}
/**
* Calls the nextMedia method in all registered MediaQueueListeners
* @param media
*/
protected void fireNextMediaEvent(Media media, Metadata metadata) {
Iterator<WeakReference<MediaQueueProcessorListener>> iterator = mediaQueueProcessorListeners.iterator();
while (iterator.hasNext()) {
WeakReference<MediaQueueProcessorListener> ref = iterator.next();
if (null == ref.get()) {
iterator.remove();
} else {
ref.get().nextMedia(media, metadata);
}
}
}
public Metadata getCurrentMetadata() {
if (mediaReader == null) {
return Metadata.create("Nothing playing", null, null);
}
return mediaReader.getMetadata();
}
}

View File

@@ -0,0 +1,31 @@
package se.slackers.locality.media.queue;
import se.slackers.locality.data.FrameStorage;
import se.slackers.locality.model.Media;
public interface MediaQueue {
/**
* Returns the name of the MediaQueue
* @return The name of the MediaQueue
*/
public String getName();
public void add(Media media);
public Media get(int index);
public void remove(int index);
public int size();
public void setMountPoint(String mountPoint);
public String getMountPoint();
public void startProcessor();
public void stopProcessor();
public boolean isProcessorRunning();
public MediaQueueProcessor getMediaQueueProcessor();
public void setMediaQueueProcessor(MediaQueueProcessor queueProcessor);
public FrameStorage getFrameStorage();
public void setFrameStorage(FrameStorage frameStorage);
}

View File

@@ -0,0 +1,123 @@
package se.slackers.locality.media.queue;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Semaphore;
import org.apache.log4j.Logger;
import se.slackers.locality.data.FrameStorage;
import se.slackers.locality.model.Media;
public class MediaQueueImpl implements MediaQueue {
private static final Logger log = Logger.getLogger(MediaQueueImpl.class);
private String mountPoint;
private String queueName;
private List<Media> queue;
private Semaphore startStop = new Semaphore(1);
private MediaQueueProcessor processor = null;
private FrameStorage frameStorage = null;
private Thread processorThread = null;
public MediaQueueImpl(String queueName) {
this.queueName = queueName;
queue = Collections.synchronizedList(new LinkedList<Media>());
}
public MediaQueueProcessor getMediaQueueProcessor() {
return processor;
}
/**
* Sets the MediaQueueProcessor to be used by the media queue. This method
* ensures that the reverse dependency is correct.
*/
public void setMediaQueueProcessor(MediaQueueProcessor processor) {
this.processor = processor;
if (this.processor.getMediaQueue() != this) {
this.processor.setMediaQueue(this);
}
}
public FrameStorage getFrameStorage() {
return frameStorage;
}
public void setFrameStorage(FrameStorage frameStorage) {
this.frameStorage = frameStorage;
}
/**
* Add media to the queue.
* @param media
*/
public void add(Media media) {
queue.add(media);
}
/**
* Returns media from a certain position in the queue.
*/
public Media get(int index) {
return queue.get(index);
}
public void remove(int index) {
if (!queue.isEmpty()) {
queue.remove(index);
}
}
public int size() {
return queue.size();
}
public String getMountPoint() {
return mountPoint;
}
public void setMountPoint(String mountPoint) {
this.mountPoint = mountPoint;
}
public synchronized boolean isProcessorRunning() {
if (processorThread == null)
return false;
return processorThread.isAlive();
}
public synchronized void startProcessor() {
log.info("Waiting to start processor ["+(processorThread == null || processorThread.isAlive() == false)+"]");
startStop.acquireUninterruptibly();
log.info("Starting processor ["+(processorThread == null || processorThread.isAlive() == false)+"]");
if (processorThread == null || processorThread.isAlive() == false) {
processor.init();
processorThread = new Thread(processor, "QueueProcessor["+getMountPoint()+"]");
processorThread.start();
}
}
public void stopProcessor() {
/*
if (isProcessorRunning()) {
processor.stopProcessing();
processor.deinit();
processorThread = null;
}
*/
log.info("Releasing startStop lock");
startStop.release();
}
public String getName() {
return queueName;
}
}

View File

@@ -0,0 +1,31 @@
package se.slackers.locality.media.queue;
import se.slackers.locality.media.reader.MediaReaderFactory;
import se.slackers.locality.model.Metadata;
import se.slackers.locality.shout.ClientListener;
/**
*
* @author bysse
*
*/
public interface MediaQueueProcessor extends Runnable, ClientListener {
public void init();
public void deinit();
/**
* Stop the processor and wait until it really is stopped.
*/
public void stopProcessing();
public void setMediaQueue(MediaQueue mediaQueue);
public MediaQueue getMediaQueue();
public void setMediaReaderFactory(MediaReaderFactory mediaReaderFactory);
public MediaReaderFactory getMediaReaderFactory();
public void addMediaQueueProcessorListener(MediaQueueProcessorListener listener);
public void removeMediaQueueProcessorListener(MediaQueueProcessorListener listener);
public Metadata getCurrentMetadata();
}

View File

@@ -0,0 +1,14 @@
package se.slackers.locality.media.queue;
import se.slackers.locality.model.Media;
import se.slackers.locality.model.Metadata;
public interface MediaQueueProcessorListener {
/**
* Called when the MediaQueue advances to the next Media in queue.
* @param media The media that will be played.
* @param metadata The metadata for the media
*/
public void nextMedia(Media media, Metadata metadata);
}

View File

@@ -0,0 +1,67 @@
package se.slackers.locality.media.queue;
import java.io.IOException;
import org.apache.log4j.Logger;
import se.slackers.locality.data.FrameStorage;
import se.slackers.locality.data.FrameStorageEntry;
import se.slackers.locality.exception.EncapsuledExceptionRuntimException;
import se.slackers.locality.exception.FrameStorageIsEmptyException;
import se.slackers.locality.media.Frame;
import se.slackers.locality.media.reader.MediaReader;
public class PreloadDataMediaQueueProcessor extends AbstractMediaQueueProcessor {
private static final Logger log = Logger.getLogger(PreloadDataMediaQueueProcessor.class);
private long maximumPreload = 5000;
private long maximumHistory = 5000;
private void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
throw new EncapsuledExceptionRuntimException(e);
}
}
/**
* {@inheritDoc}
*/
protected void readData(MediaReader reader, Frame frame) {
FrameStorage storage = getMediaQueue().getFrameStorage();
long currentTime = System.currentTimeMillis();
long lastFrameTime = currentTime;
// first purge the history
storage.purgeUntil(currentTime - maximumHistory);
try {
lastFrameTime = storage.getLastFrameTime();
// Make sure we don't use old time stamps when we store frames
if (lastFrameTime < currentTime) {
lastFrameTime = currentTime;
log.warn("FrameReader is lagging");
}
} catch (FrameStorageIsEmptyException e) {
// do nothing
}
if (lastFrameTime > currentTime + maximumPreload) {
// the maximum data preload have been reached, stall the thread for a while.
sleep(250);
return;
}
//.. read more data from the media
try {
reader.readFrame(frame);
storage.add( new FrameStorageEntry(lastFrameTime, new Frame(frame) ) );
} catch (IOException e) {
throw new EncapsuledExceptionRuntimException(e);
}
}
}

View File

@@ -0,0 +1,133 @@
package se.slackers.locality.media.reader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import org.apache.log4j.Logger;
public class ByteStreamReader {
private static final Logger log = Logger.getLogger(ByteStreamReader.class);
private InputStream inputStream = null;
private ByteBuffer buffer = null;
private byte[] tempbuffer = null;
private int readIndex = 0;
private int writeIndex = 0;
private int indexOffset = 0;
public ByteStreamReader() {
// buffer = ByteBuffer.allocateDirect(1024*1024);
// tempbuffer = new byte[65536];
buffer = ByteBuffer.allocateDirect(1024*1024);
tempbuffer = new byte[65536];
}
/**
* Returns the position of the read-cursor
* @return
*/
public int getOffset() {
return indexOffset + readIndex;
}
/**
* Gets one byte from the current position.
* @return
* @throws IOException
*/
public byte read() throws IOException {
if (readIndex >= writeIndex) {
if (0 == readData()) {
throw new EOFException();
}
}
return buffer.get(readIndex++);
}
/**
*
* @param destbuffer
* @param offset
* @param length
* @throws IOException
*/
public void read(byte [] destbuffer, int offset, int length) throws IOException {
assert length < tempbuffer.length : "The requested data is bigger than the tempbuffer";
if (readIndex + length >= writeIndex) {
if (0 == readData()) {
throw new EOFException();
}
}
buffer.position(readIndex);
buffer.get(destbuffer, offset, length);
readIndex += length;
}
private int readData() throws IOException {
if (buffer.limit() - writeIndex < tempbuffer.length) {
recycleBuffer();
}
int bytes = inputStream.read(tempbuffer, 0, tempbuffer.length);
buffer.position(writeIndex);
buffer.put(tempbuffer, 0, bytes);
writeIndex += bytes;
log.info("[" + bytes + " bytes read]");
return bytes;
}
/**
* Recycle the buffer
*/
private void recycleBuffer() {
if (readIndex <= 0) {
return;
}
log.info("Recycling ByteBuffer");
int indexAdjustment = readIndex;
int recycleIndex = 0;
// the data that is to be recycled is larger than tempbuffer
while (writeIndex - readIndex > tempbuffer.length) {
buffer.position(readIndex);
buffer.get(tempbuffer, 0, tempbuffer.length);
buffer.position(recycleIndex);
buffer.put(tempbuffer, 0, tempbuffer.length);
recycleIndex += tempbuffer.length;
readIndex += tempbuffer.length;
}
if (writeIndex - readIndex > 0) {
buffer.position(readIndex);
buffer.get(tempbuffer, 0, writeIndex - readIndex);
buffer.position(recycleIndex);
buffer.put(tempbuffer, 0, writeIndex - readIndex);
}
indexOffset += indexAdjustment;
writeIndex -= indexAdjustment;
readIndex = 0;
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
}

View File

@@ -0,0 +1,24 @@
package se.slackers.locality.media.reader;
public class ByteUtils {
/**
* Converts a byte to a string of ones and zeroes.
* @param b
* @return
*/
public static String byte2String(byte b) {
StringBuffer sb = new StringBuffer();
int value = 0x80;
for (int i = 0; i < 8; i++) {
if ((b & value) == 0) {
sb.append("0");
} else {
sb.append("1");
}
value >>= 1;
}
return sb.toString();
}
}

View File

@@ -0,0 +1,51 @@
package se.slackers.locality.media.reader;
import java.io.IOException;
import se.slackers.locality.media.Frame;
import se.slackers.locality.model.Media;
import se.slackers.locality.model.Metadata;
public interface MediaReader {
/**
* Returns true if this reader supports the format of the given media.
* @param media
* @return
*/
public boolean supports(Media media);
/**
* Opens the media file.
* @param media
* @throws IOException
*/
public void open(Media media) throws IOException ;
/**
* Reads one frame of the media.
* @param frame
* @return
* @throws IOException
*/
public Frame readFrame(Frame frame) throws IOException;
/**
* Closes the media.
* @throws IOException
*/
public void close() throws IOException;
/**
* Returns true if the end of the media is reached.
* @return
*/
public boolean eof();
/**
* Returns information about the media, artist, title. This function can be
* called multiple times but it only needs to have valid metadata after open has
* been called. It must never return null.
* @return
*/
public Metadata getMetadata();
}

View File

@@ -0,0 +1,36 @@
package se.slackers.locality.media.reader;
import java.util.ArrayList;
import java.util.List;
import se.slackers.locality.exception.CantCreateMediaReaderException;
import se.slackers.locality.model.Media;
public class MediaReaderFactory {
private List<MediaReader> mediaReaders = new ArrayList<MediaReader>();
public void addMediaReader(MediaReader mediaReader) {
mediaReaders.add(mediaReader);
}
/**
* Creates a MediaReader that can handle the given media file.
* @throws CantCreateMediaReaderException
* @param media
* @return
*/
public MediaReader getMediaReader(Media media) {
for (MediaReader reader : mediaReaders) {
if (reader.supports(media)) {
try {
return (MediaReader) reader.getClass().newInstance();
} catch (InstantiationException e) {
throw new CantCreateMediaReaderException(e);
} catch (IllegalAccessException e) {
throw new CantCreateMediaReaderException(e);
}
}
}
throw new CantCreateMediaReaderException("No supported reader found for the media file ["+media.getMediaFile()+"]");
}
}

View File

@@ -0,0 +1,43 @@
package se.slackers.locality.media.reader;
import java.io.IOException;
import se.slackers.locality.media.Frame;
import se.slackers.locality.model.Media;
import se.slackers.locality.model.Metadata;
public class SilentMediaReader implements MediaReader {
byte [] emptyFrame = new byte[] {(byte) 0xff, (byte) 0xf2, 0x10, (byte) 0xc4, 0x1b, 0x27, 0x0, 0x0, 0x0, 0x3, (byte) 0xfc, 0x0, 0x0, 0x0, 0x0, 0x4c, 0x41, 0x4d, 0x45, 0x33, 0x2e, 0x39, 0x37, 0x0, 0x0, 0x0, (byte) 0xff, (byte) 0xf2, 0x10, (byte) 0xc4, 0x1b, 0x27, 0x0, 0x0, 0x0, 0x3, (byte) 0xfc, 0x0, 0x0, 0x0, 0x0, 0x4c, 0x41, 0x4d, 0x45, 0x33, 0x2e, 0x39, 0x37, 0x0, 0x0, 0x0};
public void close() throws IOException {
}
public boolean eof() {
return true;
}
public void open(Media media) throws IOException {
}
public Frame readFrame(Frame frame) throws IOException {
frame.setLength(26);
frame.setSize(emptyFrame.length);
System.arraycopy(emptyFrame, 0, frame.getData(), 0, emptyFrame.length);
return frame;
}
/**
* This method always returns false so the reader isn't used by mistake.
*/
public boolean supports(Media media) {
return false;
}
public Metadata getMetadata() {
return Metadata.create("", "", "No media playing");
}
}

View File

@@ -0,0 +1,136 @@
package se.slackers.locality.media.reader.mp3;
public class Mp3FrameHeader {
//private static final Logger log = Logger.getLogger(Mp3FrameHeader.class);
private byte header[] = new byte[4];
private long offset;
// DUMMY, MPEG1, MPEG2, MPEG2.5
private static final int sampleRateTable[][] = {
{ 0, 44100, 22050, 11025 }, { 0, 48000, 24000, 12000 },
{ 0, 32000, 16000, 8000 }, { 0, 0, 0, 0 } };
// V1 L1, V1 L2, V1 L3, V2 L1, V2 L2 & L3
private static final int bitRateTable[][] = { { 0, 0, 0, 0, 0 },
{ 32, 32, 32, 32, 8 }, { 64, 48, 40, 48, 16 },
{ 96, 56, 48, 56, 24 }, { 128, 64, 56, 64, 32 },
{ 160, 80, 64, 80, 40 }, { 192, 96, 80, 96, 48 },
{ 224, 112, 96, 112, 56 }, { 256, 128, 112, 128, 64 },
{ 288, 160, 128, 144, 80 }, { 320, 192, 160, 160, 96 },
{ 352, 224, 192, 176, 112 }, { 384, 256, 224, 192, 128 },
{ 416, 320, 256, 224, 144 }, { 448, 384, 320, 256, 160 },
{ 0, 0, 0, 0, 0 } };
public Mp3FrameHeader() {
}
public void setData(byte [] data) {
for (int i = 0; i < 4; i++) {
header[i] = data[i];
}
}
public void setData(byte b1, byte b2, byte b3, byte b4) {
header[0] = b1;
header[1] = b2;
header[2] = b3;
header[3] = b4;
}
public byte [] getData() {
return header;
}
/*
* 7 6 5 4 3 2 1 0 128 64 32 16 8 4 2 1 0x80 0x40 0x20 0x10 0x08 0x04 0x02
* 0x01
*
* A = 10 B = 11 C = 12 D = 13 E = 14 F = 15
*/
public int getMPEGVersion() {
return 4 - ((header[1] & 0x18) >> 3);
}
public int getLayerDescription() {
return 4 - ((header[1] & 0x06) >> 1);
}
public boolean isCRCProtected() {
return (header[1] & 0x01) == 1;
}
public int getBitRate() {
int index = (header[2] & 0xf0) >> 4;
int v = getMPEGVersion();
int l = getLayerDescription();
int index2 = Math.min((v - 1) * 3 + (l - 1), 4);
return bitRateTable[index][index2] * 1000;
}
public boolean isPadded() {
return (header[2] & 0x2) == 2;
}
public int getSampleRate() {
int index = (header[2] & 0x0e) >> 2;
if (index < 0 || index > 3) {
return 0;
}
int version = getMPEGVersion();
return sampleRateTable[index][version];
}
public int getChannelMode() {
return ((header[3] & 0xC0) >> 6);
}
public int getModeExtension() {
return ((header[3] & 0x30) >> 4);
}
public boolean isCopyrighted() {
return (header[3] & 0x08) != 0;
}
public boolean isOriginal() {
return (header[3] & 0x04) != 0;
}
public int getEmphasis() {
return (header[3] & 0x03);
}
public int getFrameSize() {
int bitrate = getBitRate();
int samplerate = getSampleRate();
int padding = isPadded() ? 1 : 0;
return 144 * bitrate / (samplerate + padding);
}
public String toString() {
return "Mp3Header[" + offset + "] {" + "\n MPEG Version: "
+ getMPEGVersion() + "\n Layer description: "
+ getLayerDescription() + "\n Is CRC protected: "
+ isCRCProtected() + "\n Bitrate: " + getBitRate()
+ "\n Samplerate: " + getSampleRate() + "\n Is Padded: "
+ isPadded() + "\n Channel mode: " + getChannelMode()
+ "\n Mode Extension: " + getModeExtension() + "\n Copyright: "
+ isCopyrighted() + "\n Is original: " + isOriginal()
+ "\n Emphasis: " + getEmphasis() + "\n Frame size: "
+ getFrameSize() + "\n}";
}
public long getOffset() {
return offset;
}
public void setOffset(long offset) {
this.offset = offset;
}
}

View File

@@ -0,0 +1,133 @@
package se.slackers.locality.media.reader.mp3;
import java.io.FileInputStream;
import java.io.IOException;
import org.apache.log4j.Logger;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.Tag;
import se.slackers.locality.media.Frame;
import se.slackers.locality.media.reader.ByteStreamReader;
import se.slackers.locality.media.reader.MediaReader;
import se.slackers.locality.model.Media;
import se.slackers.locality.model.Metadata;
import se.slackers.locality.model.MetadataType;
/*
* 128kbps 44.1kHz layer II uses a lot of 418 bytes and some of 417 bytes long.
* Regardless of the bitrate of the file, a frame in an MPEG-1 file lasts for 26ms (26/1000 of a second).
*/
public class Mp3MediaReader implements MediaReader {
private static final Logger log = Logger.getLogger(Mp3MediaReader.class);
private ByteStreamReader reader = null;
private Mp3FrameHeader header = new Mp3FrameHeader();
private Metadata metadata = null;
public Mp3MediaReader() {
super();
metadata = Metadata.create("Unknown", "", "");
}
/**
* {@inheritDoc}
*/
public boolean supports(Media media) {
return media.getMediaFile().toString().toLowerCase().endsWith(".mp3");
}
/**
* {@inheritDoc}
*/
public void close() throws IOException {
if (reader != null) {
reader.getInputStream().close();
reader.setInputStream(null);
reader = null;
}
}
/**
* {@inheritDoc}
*/
public void open(Media media) throws IOException {
if (reader != null) {
close();
}
reader = new ByteStreamReader();
reader.setInputStream(new FileInputStream(media.getMediaFile()));
// try to read some id3 tags from the media
try {
AudioFile audioFile = AudioFileIO.read(media.getMediaFile());
Tag tag = audioFile.getTag();
metadata.set(MetadataType.ARTIST, tag.getFirstArtist() );
metadata.set(MetadataType.ALBUM, tag.getFirstAlbum() );
metadata.set(MetadataType.TITLE, tag.getFirstTitle() );
log.info("Opening "+metadata);
} catch (Exception e) {
log.error("Can't read tag from "+media);
e.printStackTrace();
}
}
/**
* {@inheritDoc}
*/
public boolean eof() {
try {
return reader.getInputStream().available() <= 0;
} catch (IOException e) {
return true;
}
}
/**
* {@inheritDoc}
*/
public Frame readFrame(Frame frame) throws IOException {
Mp3FrameHeader header = findMp3Header();
//log.info(header.toString());
frame.setSize(header.getFrameSize());
frame.setLength(26);
System.arraycopy(header.getData(), 0, frame.getData(), 0, 4);
reader.read(frame.getData(), 4, (int)frame.getSize()-4);
return frame;
}
/**
* {@inheritDoc}
*/
public Metadata getMetadata() {
return metadata;
}
private Mp3FrameHeader findMp3Header() throws IOException {
byte lastByte = 0;
byte currentByte = 0;
while (true) {
lastByte = currentByte;
currentByte = reader.read();
// Check for the start of the Mp3 Frame Header
if (lastByte == (byte) 0xff && (currentByte & 0xE0) == 0xE0) {
header.setOffset(reader.getOffset() - 2);
header.setData(lastByte, currentByte, reader.read(), reader.read());
//log.info("Found frame start at index [" + (header.getOffset())
// + "]");
return header;
}
}
}
}

View File

@@ -0,0 +1,20 @@
package se.slackers.locality.model;
import java.io.File;
public class Media {
private File mediaFile;
public File getMediaFile() {
return mediaFile;
}
public void setMediaFile(File mediaFile) {
this.mediaFile = mediaFile;
}
@Override
public String toString() {
return "Media: "+mediaFile.toString();
}
}

View File

@@ -0,0 +1,122 @@
package se.slackers.locality.model;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
/**
*
* @author eb
*
*/
@Entity
public class MetaTag {
private Long id;
private String name;
private List<Tag> tags;
/**
*
*/
public MetaTag() {
id = new Long(0);
name = "";
tags = new ArrayList<Tag>();
}
/**
* Adds a metatag to the specified tag. This method fixes the the bidirectional dependency.
* @see Tag#addMetaTag(MetaTag)
* @param tag
*/
public void addTag(Tag tag) {
assert tag != null : "The given tag is null";
getTags().add(tag);
tag.getMetaTags().add(this);
}
/**
*
* @return
*/
@Id
@Column(name = "id", unique = true)
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
return id;
}
/**
*
* @return
*/
@Basic
@Column(name = "name", unique = true)
public String getName() {
return name;
}
/**
*
* @return
*/
@ManyToMany(targetEntity = Tag.class, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH })
@JoinTable(name = "MetaTag_Tag", joinColumns = @JoinColumn(name = "MetaTag_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "Tag_id", referencedColumnName = "id"))
public List<Tag> getTags() {
return tags;
}
/**
*
* @param tag
*/
public void removeTag(Tag tag) {
assert tag != null : "The given tag is null";
getTags().remove(tag);
tag.getMetaTags().remove(this);
}
/**
*
* @param id
*/
public void setId(Long id) {
this.id = id;
}
/**
*
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
*
* @param tags
*/
public void setTags(List<Tag> tags) {
this.tags = tags;
}
/**
* {@inheritDoc}
*/
public String toString() {
int num = tags == null ? 0 : tags.size();
return "MetaTag [id:"+id+", name:"+name+", tags: "+num+"]";
}
}

View File

@@ -0,0 +1,52 @@
package se.slackers.locality.model;
import java.util.HashMap;
import java.util.Map;
/**
* Holds some basic information about a media file.
* @author bysse
*
*/
public class Metadata {
private Map<MetadataType, String> data = new HashMap<MetadataType, String>();
public static Metadata create(String artist, String album, String title) {
Metadata info = new Metadata();
info.set(MetadataType.ARTIST, artist);
info.set(MetadataType.ALBUM, album);
info.set(MetadataType.TITLE, title);
return info;
}
public void set(MetadataType type, String value) {
data.put(type, value);
}
public boolean has(MetadataType type) {
return data.containsKey(type);
}
public String get(MetadataType type) {
return data.get(type);
}
@Override
public String toString() {
StringBuffer builder = new StringBuffer();
builder.append("[");
for (MetadataType type : data.keySet()) {
builder.append(data.get(type));
builder.append(", ");
}
if (builder.length() > 0) {
builder.setLength(builder.length()-1);
}
builder.append("]");
return builder.toString();
}
}

View File

@@ -0,0 +1,8 @@
package se.slackers.locality.model;
public enum MetadataType {
ARTIST,
ALBUM,
TRACK,
TITLE
}

View File

@@ -0,0 +1,122 @@
package se.slackers.locality.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
/**
*
* @author eb
*
*/
@Entity
public class Tag implements Serializable {
private static final long serialVersionUID = 825416798925249763L;
private Long id;
private String name;
private List<MetaTag> metaTags;
/**
*
*/
public Tag() {
id = new Long(0);
name = "";
metaTags = new ArrayList<MetaTag>();
}
/**
* Adds a metatag to the tag. This method also fixes the the bidirectional dependency.
* @see MetaTag#addTag(Tag)
* @param metatag
*/
public void addMetaTag(MetaTag metatag) {
assert metatag != null : "The given meta tag is null";
getMetaTags().add(metatag);
metatag.getTags().add(this);
}
/**
*
* @return
*/
@Id
@Column(name = "id", unique = true)
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
return id;
}
/**
*
* @return
*/
@ManyToMany(targetEntity = MetaTag.class, cascade = { CascadeType.PERSIST, CascadeType.MERGE }, mappedBy = "tags")
public List<MetaTag> getMetaTags() {
return metaTags;
}
/**
*
* @return
*/
@Basic
@Column(name = "name", unique = true)
public String getName() {
return name;
}
/**
*
* @param metatag
*/
public void removeMetaTag(MetaTag metatag) {
assert metatag != null : "The given meta tag is null";
getMetaTags().remove(metatag);
metatag.getTags().remove(this);
}
/**
*
* @param id
*/
public void setId(Long id) {
this.id = id;
}
/**
*
* @param metaTags
*/
public void setMetaTags(List<MetaTag> metaTags) {
this.metaTags = metaTags;
}
/**
*
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* {@inheritDoc}
*/
public String toString() {
int num = metaTags == null ? 0 : metaTags.size();
return "Tag [id:" + id + ", name:" + name + ", metatags: " + num + "]";
}
}

View File

@@ -0,0 +1,32 @@
package se.slackers.locality.net;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import se.slackers.locality.exception.IllegalRequestException;
public class HttpRequest {
private static final Pattern getRegexp = Pattern.compile("get\\s+([^\\s]+).*", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private String request;
public HttpRequest(String request) {
this.request = request;
}
public String getRequestPath() throws IllegalRequestException {
Matcher matcher = getRegexp.matcher(request);
if (matcher.matches()) {
return matcher.group(1);
}
throw new IllegalRequestException("Illegal request ["+request+"]");
}
/**
* {@inheritDoc}
*/
public String toString() {
return request;
}
}

View File

@@ -0,0 +1,14 @@
package se.slackers.locality.shout;
public interface ClientListener {
/**
* Called whenever a new ShoutRunnable is started to handle a client connection.
*/
public void clientStartStreaming();
/**
* Called when a a ShoutRunnable is stopped.
*/
public void clientStopsStreaming();
}

View File

@@ -0,0 +1,267 @@
package se.slackers.locality.shout;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.log4j.Logger;
import se.slackers.locality.data.FrameStorage;
import se.slackers.locality.data.FrameStorageEntry;
import se.slackers.locality.data.MetadataManager;
import se.slackers.locality.exception.EncapsuledExceptionRuntimException;
import se.slackers.locality.exception.FrameHasNotBeenLoadedException;
import se.slackers.locality.exception.FrameIsTooOldException;
import se.slackers.locality.exception.FrameStorageIsEmptyException;
import se.slackers.locality.media.Frame;
import se.slackers.locality.media.queue.MediaQueue;
import se.slackers.locality.net.HttpRequest;
import se.slackers.locality.shout.manager.ShoutRequestManager;
public class ShoutRunnable implements Runnable {
private static Logger log = Logger.getLogger(ShoutRunnable.class);
private static final int metadataInterval = 65536;
private Socket socket;
private ShoutRequestManager requestManager;
private MetadataManager metadataManager;
protected boolean exit;
private List<WeakReference<ClientListener>> clientConnectionListeners = new ArrayList<WeakReference<ClientListener>>();
public ShoutRunnable(ShoutRequestManager requestManager, Socket clientSocket) {
this.socket = clientSocket;
this.requestManager = requestManager;
this.metadataManager = new MetadataManager();
}
public void run() {
log.info(Thread.currentThread().getName() + " started");
try {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
HttpRequest request = readRequest(in);
log.info("Received request: "+request);
MediaQueue mediaQueue = null;
try {
mediaQueue = requestManager.processRequest(request);
} catch (SecurityException e) {
writeNegativeResponse(out);
//TODO: Change the execution flow of this method
throw new IOException("Change this code please");
}
sendStartStreamResponse(mediaQueue, out);
mediaQueue.startProcessor();
mediaQueue.getMediaQueueProcessor().addMediaQueueProcessorListener(metadataManager);
metadataManager.setMetadata(mediaQueue.getMediaQueueProcessor().getCurrentMetadata());
addClientConnectionListener(mediaQueue.getMediaQueueProcessor());
fireClientStartsStreaming();
FrameStorage storage = mediaQueue.getFrameStorage();
FrameStorageEntry entry = null;
long time = System.currentTimeMillis();
int bytesSent = 0;
int total = 0;
while (true) {
try {
entry = storage.find(time);
time = entry.getStopTime();
// grab the next frame to send
Frame frame = entry.getFrame();
// Check if we have to send metadata
if (bytesSent + frame.getSize() >= metadataInterval) {
int sendBefore = metadataInterval - bytesSent;
if (sendBefore > 0) {
out.write(frame.getData(), 0, sendBefore);
}
byte [] metadata = metadataManager.getMetaData(mediaQueue);
out.write(metadata);
out.write(frame.getData(), sendBefore, frame.getSize()-sendBefore);
bytesSent -= metadataInterval;
} else {
// we don't need to send any meta data
//log.debu
out.write(entry.getFrame().getData(), 0, entry.getFrame().getSize());
}
bytesSent += frame.getSize();
total += frame.getSize();
} catch (FrameStorageIsEmptyException e) {
// sleep for a while and let the MediaQueueProcessor fill the storage
sleep(250);
} catch (FrameIsTooOldException e) {
// the client is lagging behind. Reset the time to the current system time.
time = System.currentTimeMillis();
} catch (FrameHasNotBeenLoadedException e) {
// the client is too far ahead.
//log.info("Client is too far ahead, sleeping 250 Ms");
sleep(250);
}
}
} catch(IOException e) {
e.printStackTrace();
// TODO: Fix error handling
} catch(RuntimeException e) {
e.printStackTrace();
// TODO: Fix error handling
}
fireClientStopsStreaming();
setExit(true);
log.info(Thread.currentThread().getName() + " stopped");
}
private void writeNegativeResponse(OutputStream out) throws IOException {
out.write( (new String("404 Not found")).getBytes() );
}
private void sendStartStreamResponse(MediaQueue mediaQueue, OutputStream out) throws IOException {
StringBuilder response = new StringBuilder();
response.append("HTTP/1.1 200 OK\r\nContent-Type: audio/mpeg\r\n");
// add the stream name
response.append("icy-name: ");
response.append(mediaQueue.getName());
response.append("\r\n");
// add metadata information
response.append("icy-metadata:1\r\n");
response.append("icy-metaint:");
response.append(metadataInterval);
response.append("\r\n");
response.append("\r\n");
out.write(response.toString().getBytes());
}
/**
*
* @param in
* @return
* @throws IOException
*/
private HttpRequest readRequest(InputStream in) throws IOException {
StringBuilder sb = new StringBuilder();
byte [] buffer = new byte[4096];
int bytes;
byte endSequence[] = new byte[] {13,10,13,10};
while ( (bytes=in.read(buffer)) > 0) {
sb.append(new String(buffer, 0, bytes));
if (bytes > endSequence.length) {
boolean foundSequence = true;
for (int i=0;i<endSequence.length;i++) {
foundSequence |= (endSequence[i] == buffer[bytes-i-1]);
}
if (foundSequence) {
break;
}
}
}
return new HttpRequest( sb.toString() );
}
/**
* Starts the client thread with the default name 'ShoutClientThread'.
*
*/
public void start() {
start("ShoutClientThread");
}
/**
* Starts the client thread.
* @param threadName Name of the thread
*/
public void start(String threadName) {
Thread thread = new Thread(this, threadName);
thread.start();
}
public synchronized void setExit(boolean exit) {
this.exit = exit;
}
public synchronized boolean isExit() {
return exit;
}
public void addClientConnectionListener(ClientListener listener) {
clientConnectionListeners .add(new WeakReference<ClientListener>(listener));
}
public void removeClientConnectionListener(ClientListener listener) {
clientConnectionListeners.remove(listener);
}
/**
*
*/
private void fireClientStopsStreaming() {
Iterator<WeakReference<ClientListener>> iterator = clientConnectionListeners.iterator();
while (iterator.hasNext()) {
WeakReference<ClientListener> ref = iterator.next();
if (null == ref.get()) {
iterator.remove();
} else {
ref.get().clientStopsStreaming();
}
}
}
/**
*
*/
private void fireClientStartsStreaming() {
Iterator<WeakReference<ClientListener>> iterator = clientConnectionListeners.iterator();
while (iterator.hasNext()) {
WeakReference<ClientListener> ref = iterator.next();
if (null == ref.get()) {
iterator.remove();
} else {
ref.get().clientStartStreaming();
}
}
}
/**
* Sleep for a number of milliseconds and encapsule any exception in a EncapsuledExceptionRuntimException.
* @param ms Number of milliseconds to sleep
*/
private void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
throw new EncapsuledExceptionRuntimException(e);
}
}
}

View File

@@ -0,0 +1,90 @@
package se.slackers.locality.shout;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import org.apache.log4j.Logger;
import se.slackers.locality.shout.manager.ShoutRequestManager;
/**
*
* @author bysse
*
*/
public class ShoutServer implements Runnable {
private static Logger log = Logger.getLogger(ShoutServer.class);
private ServerSocket serverSocket;
private ShoutThreadPool threadPool;
private ShoutRequestManager requestManager;
protected int serverPort = 8001;
protected boolean exitServer;
public void start() {
Thread thread = new Thread(this, "SoutServer");
thread.start();
}
public void run() {
log.info("Server started");
//.. create a listening socket
try {
serverSocket = new ServerSocket(serverPort);
} catch (IOException e) {
log.error(e);
log.info("Server stopped");
return;
}
//.. create the threads
threadPool = new ShoutThreadPool(5, "ShoutThread-");
//.. main loop
setExitServer(false);
while (!isExitServer()) {
try {
Socket clientSocket = serverSocket.accept();
ShoutRunnable client = new ShoutRunnable(requestManager, clientSocket);
threadPool.startShoutRunnable(client);
} catch (IOException e) {
e.printStackTrace();
log.error(e);
sleep(1000);
}
}
threadPool.shutdown();
setExitServer(true);
log.info("Server stopped");
}
private void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
log.error(e);
}
}
public void setServerPort(int serverPort) {
this.serverPort = serverPort;
}
public synchronized boolean isExitServer() {
return exitServer;
}
public synchronized void setExitServer(boolean exitServer) {
this.exitServer = exitServer;
}
public void setRequestManager(ShoutRequestManager requestManager) {
this.requestManager = requestManager;
}
}

View File

@@ -0,0 +1,87 @@
package se.slackers.locality.shout;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
public class ShoutThread extends Thread implements Runnable {
private static Logger log = Logger.getLogger(ShoutThread.class);
private Object targetLock = new Object();
protected List<ShoutThreadListener> listeners = new ArrayList<ShoutThreadListener>();
protected ShoutRunnable target = null;
protected boolean exit = false;
public ShoutThread(String name) {
super(name);
}
public void run() {
log.debug("Thread "+this.getName()+" is started");
while (!isExit()) {
synchronized (targetLock) {
try {
log.debug(this.getName()+": Waiting for task");
targetLock.wait();
} catch (InterruptedException e) {
log.error("Error when waiting for lock");
log.error(e);
continue;
}
}
assert target != null : "The ShoutRunnable is null for thread "+this.getName();
try {
log.debug(this.getName()+": Running task");
target.run();
} catch(Exception e) {
log.error("Thread "+this.getName()+" was aborted due to an exception", e);
}
synchronized(targetLock) {
target = null;
}
log.debug(this.getName()+": Task complete, notifying listeners");
notifyListeners();
}
log.debug("Thread "+this.getName()+" is stopped");
}
public void addShoutThreadListener(ShoutThreadListener listener) {
listeners.add(listener);
}
private void notifyListeners() {
for (ShoutThreadListener listener : listeners) {
listener.shoutThreadComplete(this);
}
}
public synchronized boolean isExit() {
return exit;
}
public synchronized void setExit(boolean exit) {
this.exit = exit;
synchronized(targetLock) {
if (target != null) {
target.setExit(exit);
}
}
}
public void setTarget(ShoutRunnable target) {
synchronized(targetLock) {
this.target = target;
targetLock.notifyAll();
}
}
}

View File

@@ -0,0 +1,5 @@
package se.slackers.locality.shout;
public interface ShoutThreadListener {
public void shoutThreadComplete(ShoutThread thread);
}

View File

@@ -0,0 +1,57 @@
package se.slackers.locality.shout;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
public class ShoutThreadPool implements ShoutThreadListener {
private static Logger log = Logger.getLogger(ShoutThreadPool.class);
private List<ShoutThread> available = new ArrayList<ShoutThread>();
private List<ShoutThread> running = new ArrayList<ShoutThread>();
public ShoutThreadPool(int size, String prefix) {
log.debug("Starting threads");
for (int i = 0; i < size; i++) {
ShoutThread thread = new ShoutThread(prefix + i);
thread.addShoutThreadListener(this);
thread.start();
available.add(thread);
}
}
public synchronized void shutdown() {
for (ShoutThread thread : available) {
thread.setExit(true);
thread.interrupt();
}
for (ShoutThread thread : running) {
thread.setExit(true);
thread.interrupt();
}
}
public synchronized void shoutThreadComplete(ShoutThread thread) {
log.debug("Recycling thread "+thread.getName());
running.remove(thread);
available.add(thread);
}
public synchronized void startShoutRunnable(ShoutRunnable runnable) {
if (available.isEmpty()) {
throw new RuntimeException("No available threads in pool");
}
ShoutThread thread = available.remove(0);
running.add(thread);
log.debug("Using thread "+thread.getName()+" for execution");
thread.setExit(false);
thread.setTarget(runnable);
}
}

View File

@@ -0,0 +1,24 @@
package se.slackers.locality.shout.manager;
import se.slackers.locality.media.queue.MediaQueue;
import se.slackers.locality.net.HttpRequest;
/**
*
* @author bysse
*
*/
public interface ShoutRequestManager {
/**
* Returns a MediaQueue object for the request or throws an exception.
* @param request
* @return
*/
public MediaQueue processRequest(HttpRequest request) throws SecurityException;
/**
* Registers a new MediaQueue.
* @param mediaQueue
*/
public void registerQueue(MediaQueue mediaQueue);
}

View File

@@ -0,0 +1,52 @@
package se.slackers.locality.shout.manager;
import java.util.HashMap;
import java.util.Map;
import org.apache.log4j.Logger;
import se.slackers.locality.exception.IllegalRequestException;
import se.slackers.locality.media.queue.MediaQueue;
import se.slackers.locality.net.HttpRequest;
/**
* Handles the mapping between a request and a MediaQueue.
* @author bysse
*
*/
public class ShoutRequestManagerImpl implements ShoutRequestManager {
private static final Logger log = Logger
.getLogger(ShoutRequestManagerImpl.class);
private Map<String, MediaQueue> mountpoints = new HashMap<String, MediaQueue>();
/**
* {@inheritDoc}
*/
public MediaQueue processRequest(HttpRequest request)
throws SecurityException {
try {
String path = request.getRequestPath();
if (mountpoints.containsKey(path)) {
return mountpoints.get(path);
}
} catch (IllegalRequestException e) {
log.error("IllegalRequest ["+request+"]");
}
throw new SecurityException("The media queue "+request+" could not be found");
}
/**
* {@inheritDoc}
*/
public void registerQueue(MediaQueue mediaQueue) {
String mountPoint = mediaQueue.getMountPoint();
if (mountpoints.containsKey(mountPoint)) {
mountpoints.remove(mountPoint);
}
mountpoints.put(mountPoint, mediaQueue);
}
}

View File

@@ -0,0 +1,16 @@
package se.slackers.locality.util;
import java.util.HashMap;
import java.util.Map;
public class OneShot {
private static Map<String, Boolean> map = new HashMap<String, Boolean>();
public static boolean test(String id) {
if (map.containsKey(id)) {
return false;
}
map.put(id, Boolean.TRUE);
return true;
}
}

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<!-- =================================================================== -->
<!-- Database configuration -->
<!-- =================================================================== -->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"
lazy-init="true" p:driverClassName="${db.driverClassName}"
p:url="${db.url}" p:username="${db.username}"
p:password="${db.password}" p:defaultTransactionIsolation="2"
p:initialSize="50" p:maxActive="50" p:maxWait="10000"
p:poolPreparedStatements="true" />
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate"
p:data-source-ref="dataSource" />
<!-- =================================================================== -->
<!-- Hibernate configuration -->
<!-- =================================================================== -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
p:dataSource-ref="dataSource">
<property name="annotatedClasses">
<list>
<value>se.slackers.locality.model.MetaTag</value>
<value>se.slackers.locality.model.Tag</value>
</list>
</property>
<property name="annotatedPackages">
<list>
<value>se.slackers.locality.model</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
${db.hibernate.dialect}
</prop>
<prop key="hibernate.jdbc.batch_size">20</prop>
<prop key="show_sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
<prop key="hibernate.max_fetch_depth">2</prop>
<prop key="hibernate.cache.provider_class">
org.hibernate.cache.NoCacheProvider
</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.autoReconnect">true</prop>
<prop key="hibernate.autoReconnectForPools">true</prop>
<prop
key="hibernate.is-connection-validation-required">
false
</prop>
<!--
<prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider
</prop>
-->
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory" />
<bean id="hibernateTemplate"
class="org.springframework.orm.hibernate3.HibernateTemplate">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- =================================================================== -->
<!-- DAO configuration -->
<!-- =================================================================== -->
<bean id="tagDao"
class="se.slackers.locality.dao.hibernate.TagDaoImpl"
p:hibernateTemplate-ref="hibernateTemplate" />
<bean id="metaTagDao"
class="se.slackers.locality.dao.hibernate.MetaTagDaoImpl"
p:hibernateTemplate-ref="hibernateTemplate" />
</beans>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<!-- property placeholder post-processor -->
<bean id="placeholderConfig"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location"
value="classpath:/conf.properties" />
</bean>
</beans>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean name="requestManager"
class="se.slackers.locality.shout.manager.ShoutRequestManagerImpl" />
<bean name="shoutServer"
class="se.slackers.locality.shout.ShoutServer"
p:requestManager.ref="requestManager" p:serverPort="8001" />
</beans>

View File

@@ -0,0 +1,5 @@
db.driverClassName=org.h2.Driver
db.url=jdbc:h2:db/locality.db
db.username=sa
db.password=
db.hibernate.dialect=org.hibernate.dialect.H2Dialect

View File

@@ -0,0 +1,16 @@
log4j.rootCategory=DEBUG, CONSOLE, LOGFILE
log4j.category.org.apache.commons=ERROR
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=DEBUG
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ddMM HH:mm} %-5p [%t] %c %x - %m%n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=logs/locality.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.Threshold=DEBUG
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ddMMyyyy HH:mm:ss,SSS} %-5p [%t] %c %x - %m%n

View File

@@ -0,0 +1,43 @@
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:hr="http://slackers.se/hr/schemas" elementFormDefault="qualified"
targetNamespace="http://slackers.se/hr/schemas">
<!--
<xs:element name="HolidayRequest">
<xs:complexType>
<xs:all>
<xs:element name="Holiday" type="hr:HolidayType" />
<xs:element name="Employee" type="hr:EmployeeType" />
</xs:all>
</xs:complexType>
</xs:element>
-->
<xs:complexType name="MetaTagType">
<xs:sequence>
<xs:element name="Name" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="TagType">
<xs:sequence>
<xs:element name="Name" type="xs:string" />
<xs:element name="MetaTags" maxOccurs="1" minOccurs="0">
<xs:complexType>
<xs:sequence>
<xs:element name="MetaTag"
type="MetaTagType">
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="EmployeeType">
<xs:sequence>
<xs:element name="Number" type="xs:integer" />
<xs:element name="FirstName" type="xs:string" />
<xs:element name="LastName" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@@ -0,0 +1,47 @@
package se.slackers.locality.dao;
import org.hibernate.SessionFactory;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
import se.slackers.locality.model.MetaTag;
public class MetaTagDaoTest extends AbstractTransactionalDataSourceSpringContextTests {
protected MetaTagDao tagDao;
protected SessionFactory sessionFactory = null;
public void testSave() {
MetaTag tag = new MetaTag();
tag.setName("testMetaTag");
tagDao.save(tag);
MetaTag fetchedMetaTag = tagDao.get("testMetaTag");
assertEquals(tag.getId(), fetchedMetaTag.getId());
}
public void testDelete() {
MetaTag tag = new MetaTag();
tag.setName("testMetaTag");
tagDao.save(tag);
tagDao.delete(tag);
try {
tagDao.get("testMetaTag");
fail();
} catch (DataRetrievalFailureException e) {
}
}
protected String[] getConfigLocations() {
return new String[] { "application-context/main.xml", "application-context/data.xml" };
}
public void setMetaTagDao(MetaTagDao tagDao) {
this.tagDao = tagDao;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
}

View File

@@ -0,0 +1,47 @@
package se.slackers.locality.dao;
import org.hibernate.SessionFactory;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
import se.slackers.locality.model.Tag;
public class TagDaoTest extends AbstractTransactionalDataSourceSpringContextTests {
protected TagDao tagDao;
protected SessionFactory sessionFactory = null;
public void testSave() {
Tag tag = new Tag();
tag.setName("testTag");
tagDao.save(tag);
Tag fetchedTag = tagDao.get("testTag");
assertEquals(tag.getId(), fetchedTag.getId());
}
public void testDelete() {
Tag tag = new Tag();
tag.setName("testMetaTag");
tagDao.save(tag);
tagDao.delete(tag);
try {
tagDao.get("testMetaTag");
fail();
} catch (DataRetrievalFailureException e) {
}
}
protected String[] getConfigLocations() {
return new String[] { "application-context/main.xml", "application-context/data.xml" };
}
public void setTagDao(TagDao tagDao) {
this.tagDao = tagDao;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
}

View File

@@ -0,0 +1,45 @@
package se.slackers.locality.dao;
import org.hibernate.SessionFactory;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
import se.slackers.locality.model.MetaTag;
import se.slackers.locality.model.Tag;
public class TagTest extends AbstractTransactionalDataSourceSpringContextTests {
protected TagDao tagDao;
protected MetaTagDao metaTagDao;
protected SessionFactory sessionFactory = null;
public void testManyToMany() {
Tag tag = new Tag();
MetaTag metatag = new MetaTag();
tag.setName("tag");
metatag.setName("metatag");
metatag.addTag(tag);
tagDao.save(tag);
metaTagDao.save(metatag);
System.out.println(tagDao.get(tag.getId()));
System.out.println(metaTagDao.get(metatag.getId()));
}
protected String[] getConfigLocations() {
return new String[] { "application-context/main.xml", "application-context/data.xml" };
}
public void setTagDao(TagDao tagDao) {
this.tagDao = tagDao;
}
public void setMetaTagDao(MetaTagDao metaTagDao) {
this.metaTagDao = metaTagDao;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
}

View File

@@ -0,0 +1,100 @@
package se.slackers.locality.data;
import junit.framework.TestCase;
import org.junit.Test;
public class CircularBufferTest extends TestCase {
private CircularBuffer buffer = new CircularBuffer(1000);
private byte [] temp = new byte[1000];
@Test
public void testReadEmpty() {
buffer.reset();
assertEquals(0, buffer.read(temp, 0, 10));
}
@Test
public void testWriteRead() {
buffer.reset();
int length = 100;
assertEquals(true, buffer.write(temp, 0, length));
assertEquals(length, buffer.read(temp, 0, length));
}
@Test
public void testWriteReadOverflow() {
buffer.reset();
int length = 100;
assertEquals(true, buffer.write(temp, 0, length));
assertEquals(length, buffer.read(temp, 0, length*2));
}
@Test
public void testWriteOverEdge() {
buffer.reset();
assertEquals(true, buffer.write(temp, 0, 900));
assertEquals(100, buffer.read(temp, 0, 100));
// should be 200 bytes free in buffer
assertEquals(true, buffer.write(temp, 0, 150));
}
@Test
public void testWriteOverEdgeLimit() {
buffer.reset();
assertEquals(true, buffer.write(temp, 0, 900));
assertEquals(100, buffer.read(temp, 0, 100));
// should be 200 bytes free in buffer
assertEquals(false, buffer.write(temp, 0, 250));
}
@Test
public void testReadOverEdge() {
buffer.reset();
assertEquals(true, buffer.write(temp, 0, 900));
assertEquals(800, buffer.read(temp, 0, 800));
// readIndex = 800, writeIndex = 900
assertEquals(true, buffer.write(temp, 0, 500));
assertEquals(500, buffer.read(temp, 0, 500));
}
@Test
public void testReadOverEdgeLimit() {
buffer.reset();
assertEquals(true, buffer.write(temp, 0, 900));
assertEquals(800, buffer.read(temp, 0, 800));
// readIndex = 800, writeIndex = 900
assertEquals(true, buffer.write(temp, 0, 500));
assertEquals(600, buffer.read(temp, 0, 700));
}
@Test
public void testRandomness() {
buffer.reset();
int size = 0;
for (int i=0;i<100;i++) {
size = 1000;
while (true) {
int len = (int)(Math.random()*400.0+100.0);
if (!buffer.write(temp, 0, len))
break;
size -= len;
}
assertTrue(size >= 0);
size = 1000-size;
while (true) {
int len = (int)(Math.random()*400.0+100.0);
int bytes = buffer.read(temp, 0, len);
size -= bytes;
if (0 == bytes)
break;
}
assertTrue(size == 0);
}
}
}

View File

@@ -0,0 +1,45 @@
package se.slackers.locality.data;
import junit.framework.TestCase;
import org.junit.Test;
public class ExpandOnWriteCircularBufferTest extends TestCase {
private static final int SIZE = 1000;
private ExpandOnWriteCircularBuffer buffer = new ExpandOnWriteCircularBuffer(SIZE);
private byte [] temp = new byte[SIZE];
@Test
public void testReadEmpty() {
buffer.reset();
assertEquals(0, buffer.read(0, temp, 0, 10));
}
@Test
public void testWriteRead() {
buffer.reset();
int length = 100;
assertEquals(true, buffer.write(temp, 0, length));
assertEquals(length, buffer.read(0, temp, 0, length));
}
@Test
public void testRandomness() {
buffer.reset();
int size = 0;
for (int i=0;i<100;i++) {
size = 0;
for (int j=0;j<10;j++) {
int len = (int)(Math.random()*400.0+100.0);
buffer.write(temp, 0, len);
size += len;
}
assertTrue(size-SIZE+1 == buffer.getReadOffset());
buffer.reset();
}
}
}

View File

@@ -0,0 +1,48 @@
package se.slackers.locality.data;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Test;
import se.slackers.locality.media.Frame;
public class FixedFrameSizeFrameStorageTest {
@Test
public void testInsertUpdate() {
FixedFrameSizeFrameStorage storage = new FixedFrameSizeFrameStorage();
// fill the storage with entries
long time = 0;
for (int i=0;i<100;i++) {
storage.add(makeEntry(time));
time += 26;
}
FrameStorageEntry entry = storage.find(6*26+3);
String value = new String(entry.getFrame().getData());
assertEquals("10011100", value);
storage.purgeUntil(10*26);
try {
storage.find(10*26-1);
fail();
} catch (RuntimeException e) {
}
}
private FrameStorageEntry makeEntry(long time) {
byte [] data = Long.toBinaryString(time).getBytes();
Frame frame = new Frame(data.length);
frame.setLength(26);
System.arraycopy(data, 0, frame.getData(), 0, data.length);
frame.setSize(data.length);
return new FrameStorageEntry(time, frame);
}
}

View File

@@ -0,0 +1,21 @@
package se.slackers.locality.data;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import se.slackers.locality.model.Metadata;
public class MetadataManagerTest {
private MetadataManager mm = new MetadataManager();
@Test
public void testParse() {
Metadata info = Metadata.create("ARTIST", "ALBUM", "TITLE");
String result = mm.parseFormat("${artist} $album$track ?(title,-) ${title}", info);
assertEquals("ARTIST ALBUM - TITLE", result);
}
}

View File

@@ -0,0 +1,41 @@
package se.slackers.locality.media.reader.mp3;
import java.io.File;
import java.io.IOException;
import junit.framework.TestCase;
import org.junit.Before;
import org.junit.Test;
import se.slackers.locality.media.Frame;
import se.slackers.locality.model.Media;
public class Mp3MediaReaderTest extends TestCase {
private static final File base = new File("target/test-classes");
private Media media = null;
private Mp3MediaReader reader = null;
@Before
public void setUp() {
media = new Media();
media.setMediaFile(new File(base, "test.mp3"));
reader = new Mp3MediaReader();
}
@Test
public void testRead() {
try {
reader.open(media);
Frame frame = new Frame(4096);
reader.readFrame(frame);
reader.readFrame(frame);
} catch (IOException e) {
e.printStackTrace();
fail();
}
}
}

View File

@@ -0,0 +1,19 @@
package se.slackers.locality.net;
import junit.framework.TestCase;
import org.junit.Test;
import se.slackers.locality.net.HttpRequest;
public class HttpRequestTest extends TestCase {
@Test
public void testRequest() throws Exception {
String httpRequest = "GET /media.mp3 HTTP/1.0\n\rHost: localhost\n\rUser-Agent: xmms/1.2.10\n\rIcy-MetaData:1\n\r\n\r";
HttpRequest request = new HttpRequest(httpRequest);
assertEquals("/media.mp3", request.getRequestPath());
}
}

View File

@@ -0,0 +1,51 @@
package se.slackers.locality.shout;
import java.io.File;
import junit.framework.TestCase;
import org.junit.Test;
import se.slackers.locality.data.FixedFrameSizeFrameStorage;
import se.slackers.locality.media.queue.MediaQueue;
import se.slackers.locality.media.queue.MediaQueueImpl;
import se.slackers.locality.media.queue.MediaQueueProcessor;
import se.slackers.locality.media.queue.PreloadDataMediaQueueProcessor;
import se.slackers.locality.media.reader.MediaReaderFactory;
import se.slackers.locality.media.reader.mp3.Mp3MediaReader;
import se.slackers.locality.model.Media;
import se.slackers.locality.shout.manager.ShoutRequestManager;
import se.slackers.locality.shout.manager.ShoutRequestManagerImpl;
public class ShoutServerTest extends TestCase {
@Test
public void testServerStart() {
Media media = new Media();
media.setMediaFile(new File("test.mp3"));
MediaReaderFactory mediaReaderFactory = new MediaReaderFactory();
mediaReaderFactory.addMediaReader(new Mp3MediaReader());
MediaQueue queue = new MediaQueueImpl("mediaQueue");
queue.setMountPoint("/media.mp3");
queue.add(media);
MediaQueueProcessor processor = new PreloadDataMediaQueueProcessor();
processor.setMediaQueue(queue);
processor.setMediaReaderFactory(mediaReaderFactory);
queue.setMediaQueueProcessor(processor);
queue.setFrameStorage( new FixedFrameSizeFrameStorage() );
ShoutRequestManager manager = new ShoutRequestManagerImpl();
manager.registerQueue(queue);
ShoutServer server = new ShoutServer();
server.setServerPort(8001);
server.setRequestManager(manager);
server.run();
}
}

Binary file not shown.

BIN
src/test/resources/test.mp3 Normal file

Binary file not shown.