import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import rotvel.kiss.Controller;
import rotvel.kiss.Kisskiss;
import rotvel.kiss.Session;
import rotvel.kiss.cache2.Cache;
import rotvel.kiss.cache2.Source;
import rotvel.kiss.fs.FileSystem2;
import rotvel.kiss.fs.Lister;
import rotvel.kiss.swtgui.Gui;
import rotvel.util.p;
import rotvel.util.logger.Logger;
/**
* 0.15
* Uses new nio caching system.
* 0.16
* Uses compiled Patterns for efficiency.
* Split up parseCommand() method into handleCmd() methods.
* 0.17
* Better handling of 'junk' commands.
* Fixed resizing bug.
* 0.18
* Logger support.
* 0.19
* Threadpool support.
*
* @version 0.19
* @author RMR
*/
public final class PlayerHandler extends Thread
{
private FileSystem2 fs;
private Controller controller;
private String playerIp;
private static int id = 0;
// Counts number of sessions for this ip
private static int sessionCount = 0;
// Hold the playerCount number for this thread
private int pCount;
final int pId;
// Check if debug output should be enabled
private static final boolean DEBUG = Kisskiss.DEBUG;
// Channel and buffer for communicating with player
private SocketChannel socketChannel;
private ByteBuffer inBuf;
private static final int BUF_SIZE = 4096;
private Charset decoder;
// The source file
private Source source;
// Holds info about current file and additional player info
private Session sessionInfo;
// While false keep running
private boolean ohNoImGoingToDie;
private LinkedList threadPool;
/**
* Constructor
* PlayerHandler
* @param controller
* @param playerIp
*/
//public PlayerHandler(Controller controller, String playerIp)
public PlayerHandler(Controller controller, LinkedList threadPool)
{
// Thread stuff
ohNoImGoingToDie = false;
setPriority(NORM_PRIORITY + 1);
//this.playerIp = playerIp;
this.controller = controller;
fs = controller.getFileSystem();
inBuf = IOUtil.getNativeBuffer(BUF_SIZE);
decoder = IOUtil.getISODecoder();
this.threadPool = threadPool;
//handling = false;
pId = id++;
}
/**
* Restarts this PlayerHandler thread and make it process commands
* from player at the other end of the SocketChannel.
* @param channel
*/
void connect(SocketChannel channel, String ipStr)
{
this.socketChannel = channel; // Simple blocking channel
this.playerIp = ipStr;
interrupt();
}
/**
* Force this PlayerHandler to terminate itself.
*/
void kill()
{
ohNoImGoingToDie = true;
interrupt();
}
// private boolean handling;
// boolean isHandling()
// {
// return handling;
// }
public void run()
{
while (!ohNoImGoingToDie)
{
try
{
synchronized (this)
{
wait();
}
}
catch (InterruptedException ignored)
{}
if (!ohNoImGoingToDie)
{
handle();
synchronized (threadPool)
{
threadPool.add(this);
}
}
}
}
private void out(String str)
{
p.ln(playerIp + "#" + pCount + " " + str);
}
private void handle()
{
pCount = ++sessionCount;
int bytesRead;
// Loop forever
for (;;)
{
try
{
// Always clear buffer before reading/writing
inBuf.clear();
// Break loop if end-of-stream has been reached
if ((bytesRead = socketChannel.read(inBuf)) == -1)
break;
inBuf.flip();
parseCommand(decoder.decode(inBuf));
}
catch (IOException e)
{
out("PlayerHandler() ioexception, breaking");
break;
}
catch (Exception e)
{
Gui.errorBox("PlayerHandler exited because of:", e);
break;
}
}
try
{
if (source != null)
{
source.close();
source = null;
}
if (sessionInfo != null && sessionInfo.isAddedToGui())
controller.removePlayer(sessionInfo);
socketChannel.close();
}
catch (IOException e)
{
out("PlayerHandler() ioexception while cleaning up");
}
}
// GET | ||
private static final Pattern GET = Pattern
.compile("(GET) (\\p{Upper}*) (.*)\\| (\\d+) (\\d+) .*", Pattern.DOTALL);
// // LIST ||
// private static final Pattern LIST = Pattern
// .compile("(LIST) (\\p{Upper}*) \\|(.*)\\|.*", Pattern.DOTALL);
// // ACTION 1 || |
// private static final Pattern ACTION1 = Pattern
// .compile("(ACTION 1) \\|(.*)\\| (\\p{Upper}*) (.*)\\|.*",
// Pattern.DOTALL);
// // ACTION 2 |
// private static final Pattern ACTION2 = Pattern
// .compile("(ACTION 2) (\\p{Upper}*) (.*)\\|.*", Pattern.DOTALL);
// // SIZE |
// private static final Pattern SIZE = Pattern
// .compile("(SIZE) (\\p{Upper}*) (.*)\\|.*", Pattern.DOTALL);
// // SIZE |
// private static final Pattern SIZE_EMPTY = Pattern.compile("(SIZE) \\|.*",
// Pattern.DOTALL);
// Known but unsupported commands
private static final Pattern JUNK = Pattern.compile("\\[audio_length:.*",
Pattern.DOTALL);
private Matcher getMatcher = GET.matcher("");
private Matcher listMatcher = LIST.matcher("");
private Matcher action1Matcher = ACTION1.matcher("");
private Matcher action2Matcher = ACTION2.matcher("");
private Matcher sizeMatcher = SIZE.matcher("");
private Matcher sizeEmptyMatcher = SIZE_EMPTY.matcher("");
private void parseCommand(CharSequence cmdLine) throws IOException
{
//out(cmdLine.toString());
// Commands are tested in order of importance. Ie: a GET should
// be processed as fast as possible etc.
// NB: group(1) is always the command name
if (getMatcher.reset(cmdLine).matches())
// type, path, offset, length
handleGet(getMatcher.group(2), getMatcher.group(3), getMatcher
.group(4), getMatcher.group(5));
else if (listMatcher.reset(cmdLine).matches())
// type, path
handleList(listMatcher.group(2), listMatcher.group(3));
else if (action1Matcher.reset(cmdLine).matches())
// playerid, type, path
handleAction(action1Matcher.group(2), action1Matcher.group(3),
action1Matcher.group(4));
else if (action2Matcher.reset(cmdLine).matches())
// type, path
handleAction(null, action2Matcher.group(2), action2Matcher.group(3));
else if (sizeMatcher.reset(cmdLine).matches())
// type, path
handleSize(sizeMatcher.group(2), sizeMatcher.group(3));
else if (sizeEmptyMatcher.reset(cmdLine).matches())
// type, path
handleSize(null, null);
else if (JUNK.matcher(cmdLine).matches())
{
p.ln("#" + pCount + ": Got junk cmd: " + cmdLine);
}
else
{
Logger.LogAndPrint("PlayerHandler got unsupported command: "
+ cmdLine);
}
}
private long getOffs;
private int getLen;
private void handleGet( String type,
String path,
String offset,
String length) throws IOException
{
// When displaying pictures the usual ACTION 1 -> SIZE -> GET
// sequence is skipped and it simply starts with a GET command.
// So we need to check if the io infrastructure is set up
if (source == null && !setupIO(type, path))
return;
getOffs = Long.parseLong(offset);
getLen = Integer.parseInt(length);
// If length==0 make sure rest of file is read
if (getLen == 0)
getLen = (int) (fileSize - getOffs);
// Retrieve wanted section of file from cache and use nio
// to transfer it to player.
source.getData(getOffs, getLen);
}
// Handle the LIST action
private void handleList(String type, String path) throws IOException
{
Lister lister = fs.list(type, path, playerIp);
writeBytes(lister.getBytes());
// Hmm, what _is_ the problem with this?
/*
* if (name.toLowerCase().endsWith(".m2v") || name
* .toLowerCase().endsWith(".m2p")) buf.append(name + "| " + path +
* FileSystem.pathSeparatorChar + name.substring(0, name.length() -
* 4) + ".mpeg|0|\n");
*
*/
}
// Type 1 is media files
// Type 2 is for retrieving auxiliary data. Eg: subtitle files
// for movies and id3 tags for mp3 files.
private boolean action1 = false;
private void handleAction(String playerId, String type, String path) throws IOException
{
// Set boolean that determines the action mode
action1 = playerId != null;
// Get currentfile
if (!setupIO(type, path))
return;
if (action1)
{
// We know now that file was found in filesystem so add
// ip and id info to playerInfo and signal controller that
// it can add player to gui if necessary.
sessionInfo.setPlayerInfo(playerId, playerIp);
controller.addPlayer(sessionInfo);
}
// Signal player that ACTION cmd went ok
writeStr("200");
}
private long fileSize;
private byte[] currentFileSizeBytes;
private void handleSize(String type, String path) throws IOException
{
// Workaround for 'empty' size
if (type == null && path == null)
writeBytes("0".getBytes());
else
{
// This is necessary when user presses 'setup' while playing file
// Appearently KiSS player doesn't cache file size.
if (currentFileSizeBytes != null || getAndSizeFile(type, path))
writeBytes(currentFileSizeBytes);
}
}
//
// IO methods
//
private boolean setupIO(String type, String fileName) throws IOException
{
if (!getAndSizeFile(type, fileName))
return false;
setFileSource();
return true;
}
private void setFileSource() throws IOException
{
Logger.LogAndPrint("PlayerHandler " + sessionInfo.getPath());
if (action1)
source = new Cache(socketChannel, sessionInfo, pCount, controller
.getCurrentCacheInMb());
else
source = new Source(socketChannel, sessionInfo, pCount);
}
private static final String CONCAT_STR = "000000000000000";
// Post: false if file wasn't found in filesystem, else true
private boolean getAndSizeFile(String type, String filename) throws IOException
{
// Find the wanted file in filesystem
sessionInfo = fs.getFile(type, filename);
if (sessionInfo == null)
{
writeStr("404");
return false;
}
else
{
// Set filesize for filelength checking in GET operation
fileSize = sessionInfo.length();
String sz = String.valueOf(fileSize);
sz = CONCAT_STR.concat(sz).substring(sz.length());
// Set filesize byte array for SIZE operation
currentFileSizeBytes = sz.getBytes();
return true;
}
}
private void writeStr(String str) throws IOException
{
writeBytes(str.getBytes());
}
private void writeBytes(byte[] data) throws IOException
{
socketChannel.write(ByteBuffer.wrap(data));
}
}