commit 6e8c389cbed0ba077882328258bf7e5ea8cc2f63 Author: erik.bystrom Date: Mon Jul 14 18:04:53 2008 +0000 Basic functionallity diff --git a/hextool.py b/hextool.py new file mode 100755 index 0000000..78bbba0 --- /dev/null +++ b/hextool.py @@ -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 diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..30b44a4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,186 @@ + + + 4.0.0 + se.slackers.locality + locality + war + 1.0 + locality + locality + http://slackers.se/app/locality + + + + + eb + + Erik Byström + + erik.bystrom+lastfmsaver@gmail.com + + Developer + + slackers.se + +1 + + + + + + + + org.springframework + spring + 2.5 + + + + + com.h2database + h2 + 1.0.67 + + + + + org.hibernate + hibernate + 3.2.6.ga + + + org.hibernate + hibernate-commons-annotations + 3.3.0.ga + + + org.hibernate + hibernate-annotations + 3.3.0.ga + + + org.springframework + spring-hibernate3 + 2.0.8 + + + + + commons-logging + commons-logging + 1.1 + + + commons-digester + commons-digester + 1.8 + + + commons-dbcp + commons-dbcp + 1.2.2 + + + + + jdom + jdom + 1.0 + + + jaxen + jaxen + 1.1.1 + + + + + jgoodies + forms + 1.0.5 + + + org.swinglabs + swingx + 0.9 + + + + + commons-httpclient + commons-httpclient + 3.1 + + + + + junit + junit + 4.4 + + + org.springframework + spring-test + 2.5 + + + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + + true + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + + + + org.apache.maven.plugins + maven-jxr-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + http://java.sun.com/j2ee/1.4/docs/api + + + http://java.sun.com/j2se/1.5.0/docs/api + + + + + + + org.apache.maven.plugins + + maven-project-info-reports-plugin + + + + + \ No newline at end of file diff --git a/src/main/java/se/slackers/jss/mediastream/MediaStream.java b/src/main/java/se/slackers/jss/mediastream/MediaStream.java new file mode 100644 index 0000000..c1744c2 --- /dev/null +++ b/src/main/java/se/slackers/jss/mediastream/MediaStream.java @@ -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; + } +} diff --git a/src/main/java/se/slackers/locality/dao/MetaTagDao.java b/src/main/java/se/slackers/locality/dao/MetaTagDao.java new file mode 100755 index 0000000..f2646ac --- /dev/null +++ b/src/main/java/se/slackers/locality/dao/MetaTagDao.java @@ -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 getLike(String name); + + /** + * Saves or updates the metatag. + * + * @param metatag + */ + public void save(MetaTag metatag); +} diff --git a/src/main/java/se/slackers/locality/dao/TagDao.java b/src/main/java/se/slackers/locality/dao/TagDao.java new file mode 100755 index 0000000..4d4cd71 --- /dev/null +++ b/src/main/java/se/slackers/locality/dao/TagDao.java @@ -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 getLike(String name); + + /** + * Saves or updates the tag. + * + * @param tag + */ + public void save(Tag tag); +} diff --git a/src/main/java/se/slackers/locality/dao/hibernate/MetaTagDaoImpl.java b/src/main/java/se/slackers/locality/dao/hibernate/MetaTagDaoImpl.java new file mode 100755 index 0000000..7ce47cc --- /dev/null +++ b/src/main/java/se/slackers/locality/dao/hibernate/MetaTagDaoImpl.java @@ -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 result = (List)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 result = (List)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 getLike(String name) { + return (List)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); + } +} diff --git a/src/main/java/se/slackers/locality/dao/hibernate/TagDaoImpl.java b/src/main/java/se/slackers/locality/dao/hibernate/TagDaoImpl.java new file mode 100755 index 0000000..d459e87 --- /dev/null +++ b/src/main/java/se/slackers/locality/dao/hibernate/TagDaoImpl.java @@ -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 result = (List)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 result = (List)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 getLike(String name) { + return (List)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); + } +} diff --git a/src/main/java/se/slackers/locality/data/CircularBuffer.java b/src/main/java/se/slackers/locality/data/CircularBuffer.java new file mode 100644 index 0000000..5394e93 --- /dev/null +++ b/src/main/java/se/slackers/locality/data/CircularBuffer.java @@ -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; + } +} diff --git a/src/main/java/se/slackers/locality/data/ExpandOnWriteCircularBuffer.java b/src/main/java/se/slackers/locality/data/ExpandOnWriteCircularBuffer.java new file mode 100644 index 0000000..ab3df97 --- /dev/null +++ b/src/main/java/se/slackers/locality/data/ExpandOnWriteCircularBuffer.java @@ -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; + } +} diff --git a/src/main/java/se/slackers/locality/data/FixedFrameSizeFrameStorage.java b/src/main/java/se/slackers/locality/data/FixedFrameSizeFrameStorage.java new file mode 100644 index 0000000..2464e70 --- /dev/null +++ b/src/main/java/se/slackers/locality/data/FixedFrameSizeFrameStorage.java @@ -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 frames = new LinkedList(); + 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 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(); + } +} diff --git a/src/main/java/se/slackers/locality/data/FrameStorage.java b/src/main/java/se/slackers/locality/data/FrameStorage.java new file mode 100644 index 0000000..905763e --- /dev/null +++ b/src/main/java/se/slackers/locality/data/FrameStorage.java @@ -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(); +} diff --git a/src/main/java/se/slackers/locality/data/FrameStorageEntry.java b/src/main/java/se/slackers/locality/data/FrameStorageEntry.java new file mode 100644 index 0000000..6abd987 --- /dev/null +++ b/src/main/java/se/slackers/locality/data/FrameStorageEntry.java @@ -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 { + 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()+")"; + } +} diff --git a/src/main/java/se/slackers/locality/data/MetadataManager.java b/src/main/java/se/slackers/locality/data/MetadataManager.java new file mode 100644 index 0000000..c71a12a --- /dev/null +++ b/src/main/java/se/slackers/locality/data/MetadataManager.java @@ -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 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; + } + } +} diff --git a/src/main/java/se/slackers/locality/exception/CantCreateMediaReaderException.java b/src/main/java/se/slackers/locality/exception/CantCreateMediaReaderException.java new file mode 100644 index 0000000..08989a4 --- /dev/null +++ b/src/main/java/se/slackers/locality/exception/CantCreateMediaReaderException.java @@ -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 + } + +} diff --git a/src/main/java/se/slackers/locality/exception/DuplicateItemException.java b/src/main/java/se/slackers/locality/exception/DuplicateItemException.java new file mode 100644 index 0000000..8a97723 --- /dev/null +++ b/src/main/java/se/slackers/locality/exception/DuplicateItemException.java @@ -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); + } +} diff --git a/src/main/java/se/slackers/locality/exception/EncapsuledExceptionRuntimException.java b/src/main/java/se/slackers/locality/exception/EncapsuledExceptionRuntimException.java new file mode 100644 index 0000000..945e06e --- /dev/null +++ b/src/main/java/se/slackers/locality/exception/EncapsuledExceptionRuntimException.java @@ -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; + } +} diff --git a/src/main/java/se/slackers/locality/exception/FrameHasNotBeenLoadedException.java b/src/main/java/se/slackers/locality/exception/FrameHasNotBeenLoadedException.java new file mode 100644 index 0000000..1a2f4a7 --- /dev/null +++ b/src/main/java/se/slackers/locality/exception/FrameHasNotBeenLoadedException.java @@ -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 + } + +} diff --git a/src/main/java/se/slackers/locality/exception/FrameIsTooOldException.java b/src/main/java/se/slackers/locality/exception/FrameIsTooOldException.java new file mode 100644 index 0000000..a1dafc3 --- /dev/null +++ b/src/main/java/se/slackers/locality/exception/FrameIsTooOldException.java @@ -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 + } + +} diff --git a/src/main/java/se/slackers/locality/exception/FrameStorageIsEmptyException.java b/src/main/java/se/slackers/locality/exception/FrameStorageIsEmptyException.java new file mode 100644 index 0000000..a0fd363 --- /dev/null +++ b/src/main/java/se/slackers/locality/exception/FrameStorageIsEmptyException.java @@ -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 + } + +} diff --git a/src/main/java/se/slackers/locality/exception/IllegalRequestException.java b/src/main/java/se/slackers/locality/exception/IllegalRequestException.java new file mode 100644 index 0000000..57f5b56 --- /dev/null +++ b/src/main/java/se/slackers/locality/exception/IllegalRequestException.java @@ -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 + } + +} diff --git a/src/main/java/se/slackers/locality/exception/InvalidBufferPositionException.java b/src/main/java/se/slackers/locality/exception/InvalidBufferPositionException.java new file mode 100644 index 0000000..dcd571d --- /dev/null +++ b/src/main/java/se/slackers/locality/exception/InvalidBufferPositionException.java @@ -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 + } + +} diff --git a/src/main/java/se/slackers/locality/media/Frame.java b/src/main/java/se/slackers/locality/media/Frame.java new file mode 100644 index 0000000..152d74c --- /dev/null +++ b/src/main/java/se/slackers/locality/media/Frame.java @@ -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(); + } +} diff --git a/src/main/java/se/slackers/locality/media/queue/AbstractMediaQueueProcessor.java b/src/main/java/se/slackers/locality/media/queue/AbstractMediaQueueProcessor.java new file mode 100644 index 0000000..6c82beb --- /dev/null +++ b/src/main/java/se/slackers/locality/media/queue/AbstractMediaQueueProcessor.java @@ -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> mediaQueueProcessorListeners = new ArrayList>(); + + 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(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> iterator = mediaQueueProcessorListeners.iterator(); + while (iterator.hasNext()) { + WeakReference 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(); + } +} diff --git a/src/main/java/se/slackers/locality/media/queue/MediaQueue.java b/src/main/java/se/slackers/locality/media/queue/MediaQueue.java new file mode 100644 index 0000000..df7e865 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/queue/MediaQueue.java @@ -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); +} diff --git a/src/main/java/se/slackers/locality/media/queue/MediaQueueImpl.java b/src/main/java/se/slackers/locality/media/queue/MediaQueueImpl.java new file mode 100644 index 0000000..a42506b --- /dev/null +++ b/src/main/java/se/slackers/locality/media/queue/MediaQueueImpl.java @@ -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 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()); + } + + 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; + } +} diff --git a/src/main/java/se/slackers/locality/media/queue/MediaQueueProcessor.java b/src/main/java/se/slackers/locality/media/queue/MediaQueueProcessor.java new file mode 100644 index 0000000..cdc23e5 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/queue/MediaQueueProcessor.java @@ -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(); +} diff --git a/src/main/java/se/slackers/locality/media/queue/MediaQueueProcessorListener.java b/src/main/java/se/slackers/locality/media/queue/MediaQueueProcessorListener.java new file mode 100644 index 0000000..c34c0d2 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/queue/MediaQueueProcessorListener.java @@ -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); +} diff --git a/src/main/java/se/slackers/locality/media/queue/PreloadDataMediaQueueProcessor.java b/src/main/java/se/slackers/locality/media/queue/PreloadDataMediaQueueProcessor.java new file mode 100644 index 0000000..fbc5a06 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/queue/PreloadDataMediaQueueProcessor.java @@ -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); + } + } +} diff --git a/src/main/java/se/slackers/locality/media/reader/ByteStreamReader.java b/src/main/java/se/slackers/locality/media/reader/ByteStreamReader.java new file mode 100644 index 0000000..f56727f --- /dev/null +++ b/src/main/java/se/slackers/locality/media/reader/ByteStreamReader.java @@ -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; + } +} diff --git a/src/main/java/se/slackers/locality/media/reader/ByteUtils.java b/src/main/java/se/slackers/locality/media/reader/ByteUtils.java new file mode 100644 index 0000000..dc8f78a --- /dev/null +++ b/src/main/java/se/slackers/locality/media/reader/ByteUtils.java @@ -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(); + } +} diff --git a/src/main/java/se/slackers/locality/media/reader/MediaReader.java b/src/main/java/se/slackers/locality/media/reader/MediaReader.java new file mode 100644 index 0000000..abe95d1 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/reader/MediaReader.java @@ -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(); +} diff --git a/src/main/java/se/slackers/locality/media/reader/MediaReaderFactory.java b/src/main/java/se/slackers/locality/media/reader/MediaReaderFactory.java new file mode 100644 index 0000000..0da4036 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/reader/MediaReaderFactory.java @@ -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 mediaReaders = new ArrayList(); + + 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()+"]"); + } +} diff --git a/src/main/java/se/slackers/locality/media/reader/SilentMediaReader.java b/src/main/java/se/slackers/locality/media/reader/SilentMediaReader.java new file mode 100644 index 0000000..9d63062 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/reader/SilentMediaReader.java @@ -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"); + } + +} diff --git a/src/main/java/se/slackers/locality/media/reader/mp3/Mp3FrameHeader.java b/src/main/java/se/slackers/locality/media/reader/mp3/Mp3FrameHeader.java new file mode 100644 index 0000000..378b533 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/reader/mp3/Mp3FrameHeader.java @@ -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; + } +} diff --git a/src/main/java/se/slackers/locality/media/reader/mp3/Mp3MediaReader.java b/src/main/java/se/slackers/locality/media/reader/mp3/Mp3MediaReader.java new file mode 100644 index 0000000..ef8bcd8 --- /dev/null +++ b/src/main/java/se/slackers/locality/media/reader/mp3/Mp3MediaReader.java @@ -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; + } + } + } +} diff --git a/src/main/java/se/slackers/locality/model/Media.java b/src/main/java/se/slackers/locality/model/Media.java new file mode 100644 index 0000000..a973f62 --- /dev/null +++ b/src/main/java/se/slackers/locality/model/Media.java @@ -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(); + } +} diff --git a/src/main/java/se/slackers/locality/model/MetaTag.java b/src/main/java/se/slackers/locality/model/MetaTag.java new file mode 100755 index 0000000..b46d02f --- /dev/null +++ b/src/main/java/se/slackers/locality/model/MetaTag.java @@ -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 tags; + + /** + * + */ + public MetaTag() { + id = new Long(0); + name = ""; + tags = new ArrayList(); + } + + /** + * 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 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 tags) { + this.tags = tags; + } + + /** + * {@inheritDoc} + */ + public String toString() { + int num = tags == null ? 0 : tags.size(); + return "MetaTag [id:"+id+", name:"+name+", tags: "+num+"]"; + } +} diff --git a/src/main/java/se/slackers/locality/model/Metadata.java b/src/main/java/se/slackers/locality/model/Metadata.java new file mode 100644 index 0000000..90dca94 --- /dev/null +++ b/src/main/java/se/slackers/locality/model/Metadata.java @@ -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 data = new HashMap(); + + 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(); + } +} diff --git a/src/main/java/se/slackers/locality/model/MetadataType.java b/src/main/java/se/slackers/locality/model/MetadataType.java new file mode 100644 index 0000000..8d256fa --- /dev/null +++ b/src/main/java/se/slackers/locality/model/MetadataType.java @@ -0,0 +1,8 @@ +package se.slackers.locality.model; + +public enum MetadataType { + ARTIST, + ALBUM, + TRACK, + TITLE +} diff --git a/src/main/java/se/slackers/locality/model/Tag.java b/src/main/java/se/slackers/locality/model/Tag.java new file mode 100755 index 0000000..80c9071 --- /dev/null +++ b/src/main/java/se/slackers/locality/model/Tag.java @@ -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 metaTags; + + /** + * + */ + public Tag() { + id = new Long(0); + name = ""; + metaTags = new ArrayList(); + } + + /** + * 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 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 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 + "]"; + } +} diff --git a/src/main/java/se/slackers/locality/net/HttpRequest.java b/src/main/java/se/slackers/locality/net/HttpRequest.java new file mode 100644 index 0000000..ddc72fa --- /dev/null +++ b/src/main/java/se/slackers/locality/net/HttpRequest.java @@ -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; + } +} diff --git a/src/main/java/se/slackers/locality/shout/ClientListener.java b/src/main/java/se/slackers/locality/shout/ClientListener.java new file mode 100644 index 0000000..3086953 --- /dev/null +++ b/src/main/java/se/slackers/locality/shout/ClientListener.java @@ -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(); + +} diff --git a/src/main/java/se/slackers/locality/shout/ShoutRunnable.java b/src/main/java/se/slackers/locality/shout/ShoutRunnable.java new file mode 100644 index 0000000..d767add --- /dev/null +++ b/src/main/java/se/slackers/locality/shout/ShoutRunnable.java @@ -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> clientConnectionListeners = new ArrayList>(); + + 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(listener)); + } + + public void removeClientConnectionListener(ClientListener listener) { + clientConnectionListeners.remove(listener); + } + + /** + * + */ + private void fireClientStopsStreaming() { + Iterator> iterator = clientConnectionListeners.iterator(); + while (iterator.hasNext()) { + WeakReference ref = iterator.next(); + + if (null == ref.get()) { + iterator.remove(); + } else { + ref.get().clientStopsStreaming(); + } + } + } + + /** + * + */ + private void fireClientStartsStreaming() { + Iterator> iterator = clientConnectionListeners.iterator(); + while (iterator.hasNext()) { + WeakReference 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); + } + } +} diff --git a/src/main/java/se/slackers/locality/shout/ShoutServer.java b/src/main/java/se/slackers/locality/shout/ShoutServer.java new file mode 100644 index 0000000..5853063 --- /dev/null +++ b/src/main/java/se/slackers/locality/shout/ShoutServer.java @@ -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; + } +} diff --git a/src/main/java/se/slackers/locality/shout/ShoutThread.java b/src/main/java/se/slackers/locality/shout/ShoutThread.java new file mode 100644 index 0000000..e7895ad --- /dev/null +++ b/src/main/java/se/slackers/locality/shout/ShoutThread.java @@ -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 listeners = new ArrayList(); + 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(); + } + } + +} diff --git a/src/main/java/se/slackers/locality/shout/ShoutThreadListener.java b/src/main/java/se/slackers/locality/shout/ShoutThreadListener.java new file mode 100644 index 0000000..0be8c23 --- /dev/null +++ b/src/main/java/se/slackers/locality/shout/ShoutThreadListener.java @@ -0,0 +1,5 @@ +package se.slackers.locality.shout; + +public interface ShoutThreadListener { + public void shoutThreadComplete(ShoutThread thread); +} diff --git a/src/main/java/se/slackers/locality/shout/ShoutThreadPool.java b/src/main/java/se/slackers/locality/shout/ShoutThreadPool.java new file mode 100644 index 0000000..35666e7 --- /dev/null +++ b/src/main/java/se/slackers/locality/shout/ShoutThreadPool.java @@ -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 available = new ArrayList(); + + private List running = new ArrayList(); + + 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); + } + +} diff --git a/src/main/java/se/slackers/locality/shout/manager/ShoutRequestManager.java b/src/main/java/se/slackers/locality/shout/manager/ShoutRequestManager.java new file mode 100644 index 0000000..276836a --- /dev/null +++ b/src/main/java/se/slackers/locality/shout/manager/ShoutRequestManager.java @@ -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); +} diff --git a/src/main/java/se/slackers/locality/shout/manager/ShoutRequestManagerImpl.java b/src/main/java/se/slackers/locality/shout/manager/ShoutRequestManagerImpl.java new file mode 100644 index 0000000..8258bc0 --- /dev/null +++ b/src/main/java/se/slackers/locality/shout/manager/ShoutRequestManagerImpl.java @@ -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 mountpoints = new HashMap(); + + /** + * {@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); + } +} diff --git a/src/main/java/se/slackers/locality/util/OneShot.java b/src/main/java/se/slackers/locality/util/OneShot.java new file mode 100644 index 0000000..dc34328 --- /dev/null +++ b/src/main/java/se/slackers/locality/util/OneShot.java @@ -0,0 +1,16 @@ +package se.slackers.locality.util; + +import java.util.HashMap; +import java.util.Map; + +public class OneShot { + private static Map map = new HashMap(); + + public static boolean test(String id) { + if (map.containsKey(id)) { + return false; + } + map.put(id, Boolean.TRUE); + return true; + } +} diff --git a/src/main/resources/application-context/data.xml b/src/main/resources/application-context/data.xml new file mode 100755 index 0000000..fd4249f --- /dev/null +++ b/src/main/resources/application-context/data.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + se.slackers.locality.model.MetaTag + se.slackers.locality.model.Tag + + + + + se.slackers.locality.model + + + + + + ${db.hibernate.dialect} + + 20 + true + create + 2 + + org.hibernate.cache.NoCacheProvider + + true + true + true + + false + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/application-context/main.xml b/src/main/resources/application-context/main.xml new file mode 100755 index 0000000..3fb4fb9 --- /dev/null +++ b/src/main/resources/application-context/main.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/application-context/shout.xml b/src/main/resources/application-context/shout.xml new file mode 100644 index 0000000..9d4de7e --- /dev/null +++ b/src/main/resources/application-context/shout.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/conf.properties b/src/main/resources/conf.properties new file mode 100755 index 0000000..199e656 --- /dev/null +++ b/src/main/resources/conf.properties @@ -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 \ No newline at end of file diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100755 index 0000000..db34227 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -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 \ No newline at end of file diff --git a/src/main/resources/schemas/locality.xsd b/src/main/resources/schemas/locality.xsd new file mode 100755 index 0000000..fe514c3 --- /dev/null +++ b/src/main/resources/schemas/locality.xsd @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/se/slackers/locality/dao/MetaTagDaoTest.java b/src/test/java/se/slackers/locality/dao/MetaTagDaoTest.java new file mode 100755 index 0000000..e90c302 --- /dev/null +++ b/src/test/java/se/slackers/locality/dao/MetaTagDaoTest.java @@ -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; + } +} \ No newline at end of file diff --git a/src/test/java/se/slackers/locality/dao/TagDaoTest.java b/src/test/java/se/slackers/locality/dao/TagDaoTest.java new file mode 100755 index 0000000..1065079 --- /dev/null +++ b/src/test/java/se/slackers/locality/dao/TagDaoTest.java @@ -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; + } +} diff --git a/src/test/java/se/slackers/locality/dao/TagTest.java b/src/test/java/se/slackers/locality/dao/TagTest.java new file mode 100755 index 0000000..410c79d --- /dev/null +++ b/src/test/java/se/slackers/locality/dao/TagTest.java @@ -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; + } +} \ No newline at end of file diff --git a/src/test/java/se/slackers/locality/data/CircularBufferTest.java b/src/test/java/se/slackers/locality/data/CircularBufferTest.java new file mode 100644 index 0000000..b8124b4 --- /dev/null +++ b/src/test/java/se/slackers/locality/data/CircularBufferTest.java @@ -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); + } + } +} diff --git a/src/test/java/se/slackers/locality/data/ExpandOnWriteCircularBufferTest.java b/src/test/java/se/slackers/locality/data/ExpandOnWriteCircularBufferTest.java new file mode 100644 index 0000000..e80faf9 --- /dev/null +++ b/src/test/java/se/slackers/locality/data/ExpandOnWriteCircularBufferTest.java @@ -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(); + } + } +} diff --git a/src/test/java/se/slackers/locality/data/FixedFrameSizeFrameStorageTest.java b/src/test/java/se/slackers/locality/data/FixedFrameSizeFrameStorageTest.java new file mode 100644 index 0000000..97687a7 --- /dev/null +++ b/src/test/java/se/slackers/locality/data/FixedFrameSizeFrameStorageTest.java @@ -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); + } +} diff --git a/src/test/java/se/slackers/locality/data/MetadataManagerTest.java b/src/test/java/se/slackers/locality/data/MetadataManagerTest.java new file mode 100644 index 0000000..e9c0403 --- /dev/null +++ b/src/test/java/se/slackers/locality/data/MetadataManagerTest.java @@ -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); + } +} diff --git a/src/test/java/se/slackers/locality/media/reader/mp3/Mp3MediaReaderTest.java b/src/test/java/se/slackers/locality/media/reader/mp3/Mp3MediaReaderTest.java new file mode 100644 index 0000000..e2b1fbe --- /dev/null +++ b/src/test/java/se/slackers/locality/media/reader/mp3/Mp3MediaReaderTest.java @@ -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(); + } + } +} diff --git a/src/test/java/se/slackers/locality/net/HttpRequestTest.java b/src/test/java/se/slackers/locality/net/HttpRequestTest.java new file mode 100644 index 0000000..b5a378e --- /dev/null +++ b/src/test/java/se/slackers/locality/net/HttpRequestTest.java @@ -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()); + } + +} diff --git a/src/test/java/se/slackers/locality/shout/ShoutServerTest.java b/src/test/java/se/slackers/locality/shout/ShoutServerTest.java new file mode 100644 index 0000000..46da7e9 --- /dev/null +++ b/src/test/java/se/slackers/locality/shout/ShoutServerTest.java @@ -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(); + } +} diff --git a/src/test/resources/silence.mp3 b/src/test/resources/silence.mp3 new file mode 100644 index 0000000..65056b9 Binary files /dev/null and b/src/test/resources/silence.mp3 differ diff --git a/src/test/resources/test.mp3 b/src/test/resources/test.mp3 new file mode 100644 index 0000000..4f60300 Binary files /dev/null and b/src/test/resources/test.mp3 differ