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)); } }