From ede91322798dbd24eb9e86c4d4fe48315ef93637 Mon Sep 17 00:00:00 2001 From: Markil3 <75867393+Markil3@users.noreply.github.com> Date: Mon, 16 Aug 2021 22:42:44 -0600 Subject: [PATCH] Adds the ability to play youtube videos. It still needs a bit of work, but playing and pausing are operational. --- add-on/background.js | 149 ++++++++++++- add-on/foreground.js | 82 +++++++ add-on/manifest.json | 7 + add-on/youtube.js | 76 +++++++ addonInter/build.gradle | 5 + .../universeplayer/addon/BrowserLink.java | 21 +- .../edu/regis/universeplayer/addon/Main.java | 53 ++--- .../addon/StackTraceElementSerializer.java | 33 +++ .../addon/ThrowableSerializer.java | 54 +++++ addonInter/src/main/resources/log4j2.xml | 4 + browserCommands/build.gradle | 5 + .../regis/universeplayer/PlaybackStatus.java | 0 .../browserCommands/BrowserConstants.java | 39 +++- .../browserCommands/CommandQuit.java | 25 +++ .../browserCommands/MessageHandler.java | 22 +- .../browserCommands/MessageRunner.java | 36 +-- .../browserCommands/QueryStatus.java | 28 +++ interface/build.gradle | 3 + .../regis/universeplayer/browser/Browser.java | 210 ++++++++++++++---- .../localPlayer/LocalPlayer.java | 8 +- .../universeplayer/player/Interface.java | 74 ++++-- .../universeplayer/player/PlayerControls.java | 3 +- interface/src/main/resources/log4j2.xml | 2 + 23 files changed, 799 insertions(+), 140 deletions(-) create mode 100644 add-on/foreground.js create mode 100644 add-on/youtube.js create mode 100644 addonInter/src/main/java/edu/regis/universeplayer/addon/StackTraceElementSerializer.java create mode 100644 addonInter/src/main/java/edu/regis/universeplayer/addon/ThrowableSerializer.java rename {interface => browserCommands}/src/main/java/edu/regis/universeplayer/PlaybackStatus.java (100%) create mode 100644 browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandQuit.java create mode 100644 browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryStatus.java diff --git a/add-on/background.js b/add-on/background.js index 5142b5c..6fbddc2 100644 --- a/add-on/background.js +++ b/add-on/background.js @@ -1,10 +1,112 @@ console.log("Hello from Universal Music addon!") +const tabMessages = new Map(); +var numTabMessages = 0; /* On startup, connect to the "ping_pong" app. */ var port = browser.runtime.connectNative("universalmusic"); +var tabPort = null; + +function setupTab(port) +{ + console.info("Tab " + port.sender.tab.url + " loaded."); + tabPort = port; + tabPort.onDisconnect.addListener(e => { + tabPort = null; + if (e.error) + { + console.error("Tab error: ", e.error) + } + else + { + console.info("Tab " + e.sender.tab.url + " disconnected."); + } + browser.tabs.remove(e.sender.tab.id); + }); + tabPort.onMessage.addListener(message => { + if (typeof message == "object" && message.type == "response") + { + tabMessages.set(message.num, message.data); + } + }); + return true; +} + +function queryTab(message) +{ + let num = numTabMessages++; + let promise = new Promise((resolve, reject) => { + tabPort.postMessage({ + num: num, + data: message + }); + (function awaitResponse() { + if (tabMessages.has(num)) + { + response = tabMessages.get(num); + tabMessages.delete(num); + return resolve(response); + } + else + { + setTimeout(awaitResponse, 30); + } + })(); + }); + tabMessages.set(numTabMessages, promise); + numTabMessages++; + return promise; +} + var listeners = [function (message, returnValue) { + if (typeof message == "object" && "type" in message) + { + let type = message.type; + lastPeriod = type.lastIndexOf("."); + if (lastPeriod != -1) + { + type = type.substring(lastPeriod + 1); + } + switch (type) + { + case "CommandSong": + returnValue = () => browser.tabs.create({url: message.song}).then(setupTab); + if (tabPort) + { + console.log("Replacing tab"); + /* + * Closes the existing tab first + */ + returnValue = browser.tabs.remove(e.sender.tab.id).then(returnValue); + } + else + { + console.log("Opening tab"); + returnValue = returnValue(); + } + break; + case "QueryStatus": + if (!tabPort) + { + returnValue = 4; + break; + } + case "QueryTime": + case "QueryLength": + case "CommandSetPlayback": + case "CommandSeek": + returnValue = queryTab(message); + break; + case "CommandQuit": + returnValue = quit(); + break; + } + } + else if (message == "quit") + { + returnValue = quit(); + } if (returnValue == null) { returnValue = "pong"; @@ -12,6 +114,17 @@ var listeners = [function (message, returnValue) { return returnValue; }]; +function quit() +{ + console.log("Quitting browser"); + return browser.tabs.query({}).then(tabs => { + for (let tab of tabs) + { + browser.tabs.remove(tab.id); + } + }); +} + function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -20,11 +133,25 @@ function handleMessage(message) { return new Promise((resolve, reject) => { var returnValue = null; - for (index in listeners) + try { - returnValue = listeners[index](message, returnValue); + for (index in listeners) + { + returnValue = listeners[index](message, returnValue); + } + if (returnValue instanceof Promise) + { + returnValue.then(resolve).catch(reject); + } + else + { + resolve(returnValue); + } + } + catch (e) + { + reject(e); } - resolve(returnValue); }) } @@ -32,14 +159,26 @@ function handleMessage(message) * Listen for messages from the app. */ port.onMessage.addListener((message) => { - console.log("Received: ", message); + console.log("Received from interface: ", message); handleMessage(message.message).then((response) => { returnValue = { "messageNum": message.messageNum, - "message": response + "message": { + "type": "edu.regis.universeplayer.browserCommands.CommandReturn", + "returnValue": response, + "confirmation": { + type: "edu.regis.universeplayer.browserCommands.CommandConfirmation", + message: "Done", + errorCode: null + } + } } console.log("Sending ", returnValue) port.postMessage(returnValue); + }).catch(error => { + }); }); + +browser.runtime.onConnect.addListener(setupTab); diff --git a/add-on/foreground.js b/add-on/foreground.js new file mode 100644 index 0000000..74400f1 --- /dev/null +++ b/add-on/foreground.js @@ -0,0 +1,82 @@ +console.debug("Loading foreground.js") +let background; + +function handleMessage(message) +{ + if (typeof message == "object" && "type" in message) + { + let type = message.type; + lastPeriod = type.lastIndexOf("."); + if (lastPeriod != -1) + { + type = type.substring(lastPeriod + 1); + } + switch (type) + { + case "QueryStatus": + return getState(); + case "QueryTime": + return getTime(); + case "QueryLength": + return getLength(); + case "CommandSetPlayback": + switch (message.status) + { + case "PLAY": + return play(); + case "PAUSE": + return pause(); + } + return false; + case "CommandSeek": + return seek(message.time); + } + } +} + +function sendUpdate(response) +{ + post = data => background.postMessage({ + type: "update", + data: data + }); + if (response instanceof Promise) + { + response.then(post); + } + else + { + post(response); + } +} + +background = browser.runtime.connect({name:"universalMusic"}); +background.onMessage.addListener(message => { + let num = message.num; + response = handleMessage(message.data); + post = data => background.postMessage({ + type: "response", + num: num, + data: data + }); + if (response instanceof Promise) + { + response.then(post); + } + else + { + post(response); + } +}); + +if (document.readyState === "complete") +{ + sendUpdate("loaded"); +} +else +{ + sendUpdate("loaded"); + window.addEventListener("load", () => { + sendUpdate("loaded"); + }); +} \ No newline at end of file diff --git a/add-on/manifest.json b/add-on/manifest.json index 922cb50..8abb092 100644 --- a/add-on/manifest.json +++ b/add-on/manifest.json @@ -22,6 +22,13 @@ "scripts": ["background.js"] }, + "content_scripts": [ + { + "matches": ["*://*.youtube.com/watch?v=*"], + "js": ["foreground.js", "youtube.js"] + } + ], + "browser_action": { "default_icon": "icon.svg" }, diff --git a/add-on/youtube.js b/add-on/youtube.js new file mode 100644 index 0000000..7dfcd87 --- /dev/null +++ b/add-on/youtube.js @@ -0,0 +1,76 @@ +console.debug("Loading youtube.js") +var video; + +function onload() +{ + video = document.getElementsByClassName('video-stream html5-main-video')[0] +} + +function getState() +{ + if (!video) + { + return 4; + } + else if (video.ended) + { + return 3; + } + else if (video.paused) + { + return 1; + } + else + { + return 0; + } +} + +function getTime() +{ + return video.currentTime; +} + +function getLength() +{ + return video.duration; +} + +function play() +{ + if (video != null) + { + video.play(); + return true; + } + return false; +} + +function pause() +{ + if (video != null) + { + video.pause(); + return true; + } + return false; +} + +function seek(time) +{ + if (video != null) + { + video.currentTime = time; + return true; + } + return false; +} + +if (document.readyState === "complete") +{ + onload(); +} +else +{ + window.addEventListener("load", onload); +} \ No newline at end of file diff --git a/addonInter/build.gradle b/addonInter/build.gradle index 7649933..65ac454 100644 --- a/addonInter/build.gradle +++ b/addonInter/build.gradle @@ -20,6 +20,9 @@ plugins { // In this section you declare where to find the dependencies of your project repositories { mavenCentral() + flatDir { + dirs new File(rootDir, 'libs') + } } // In this section you declare the dependencies for your production and test code @@ -31,6 +34,8 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.7' + implementation 'net.harawata:appdirs:1.2.1' + implementation project(":browserCommands") // Declare the dependency for your favourite test framework you want to use in your tests. diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java index c7b56a7..36e4291 100644 --- a/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java @@ -27,8 +27,16 @@ import java.util.HashMap; */ public class BrowserLink extends MessageRunner { -// private static final Logger logger = LoggerFactory.getLogger(BrowserLink.class); - public static final Gson gson = new Gson(); + private static final Logger logger = LoggerFactory.getLogger(BrowserLink.class); + public static final Gson gson; + + static + { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(StackTraceElement.class, new StackTraceElementSerializer()); + builder.registerTypeAdapter(Throwable.class, new ThrowableSerializer()); + gson = builder.create(); + } /** * Creates a message runner. @@ -41,8 +49,12 @@ public class BrowserLink extends MessageRunner @Override public byte[] serializeObject(Object message) { - String val = gson.toJson(message); - return val.getBytes(StandardCharsets.UTF_8); + JsonElement val = gson.toJsonTree(message); + if (val.isJsonObject()) + { + ((JsonObject) val).addProperty("type", message.getClass().getName()); + } + return val.toString().getBytes(StandardCharsets.UTF_8); } @Override @@ -197,6 +209,7 @@ public class BrowserLink extends MessageRunner try { clazz = Class.forName(message.getAsJsonPrimitive("type").getAsString()); + message.remove("type"); } catch (ClassNotFoundException | ClassCastException e) { diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java index 546c062..9c38a50 100644 --- a/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java @@ -10,7 +10,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.net.ServerSocket; import java.net.Socket; import java.util.LinkedList; import java.util.concurrent.ExecutionException; @@ -22,23 +21,33 @@ public class Main public static void main(String[] args) throws IOException, InterruptedException { - ServerSocket socketSource = null; - Socket socket = null; MessageHandler interfaceLink; BrowserLink browserLink; Thread interfaceThread, browserThread; + Socket socket; try { + logger.debug("Connecting to browser"); + browserLink = new BrowserLink("BrowserLink"); try { - logger.debug("Connecting to browser"); - browserLink = new BrowserLink("BrowserLink"); - logger.debug("Setting up server"); - socketSource = new ServerSocket(BrowserConstants.PORT); - logger.debug("Server started"); - socket = socketSource.accept(); + logger.debug("Setting up connection"); + socket = new Socket(BrowserConstants.IP,BrowserConstants.PORT); + logger.debug("Connection established"); + + interfaceLink = new MessageHandler("InterfaceHandler", socket.getInputStream(), socket.getOutputStream()) { + @Override + protected boolean onRun() + { + if (!socket.isConnected() || socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) + { + logger.debug("Socket closed, shutting down"); + return true; + } + return false; + } + }; logger.debug("Connection received"); - interfaceLink = new MessageHandler("InterfaceHandler", socket.getInputStream(), socket.getOutputStream()); /* * Pretty much just forwards any messages to the browser and * returns their value. @@ -64,6 +73,7 @@ public class Main catch (IOException e) { logger.error("Could not initialize socket", e); + browserLink.sendObject("quit"); throw e; } } @@ -75,29 +85,6 @@ public class Main finally { logger.debug("Shutting down interface link"); - if (socket != null) - { - try - { - socket.close(); - } - catch (IOException e) - { - logger.error("Could not close socket", e); - } - finally - { - logger.debug("Shutting down server"); - try - { - socketSource.close(); - } - catch (IOException e) - { - logger.error("Could not close server", e); - } - } - } } } diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/StackTraceElementSerializer.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/StackTraceElementSerializer.java new file mode 100644 index 0000000..c99abb5 --- /dev/null +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/StackTraceElementSerializer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.addon; + +import com.google.gson.*; + +import java.lang.reflect.Type; + +public class StackTraceElementSerializer implements JsonSerializer, JsonDeserializer +{ + @Override + public JsonElement serialize(StackTraceElement src, Type typeOfSrc, JsonSerializationContext context) + { + JsonObject ob = new JsonObject(); + ob.addProperty("loader", src.getClassLoaderName()); + ob.addProperty("class", src.getClassName()); + ob.addProperty("file", src.getFileName()); + ob.addProperty("line", src.getLineNumber()); + ob.addProperty("method", src.getMethodName()); + ob.addProperty("module", src.getModuleName()); + ob.addProperty("version", src.getModuleVersion()); + return ob; + } + + @Override + public StackTraceElement deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + JsonObject ob = json.getAsJsonObject(); + return new StackTraceElement(ob.get("loader").getAsString(), ob.get("module").getAsString(), ob.get("version").getAsString(), ob.get("class").getAsString(), ob.get("method").getAsString(), ob.get("file").getAsString(), ob.get("line").getAsInt()); + } +} diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/ThrowableSerializer.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/ThrowableSerializer.java new file mode 100644 index 0000000..b949496 --- /dev/null +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/ThrowableSerializer.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.addon; + +import com.google.gson.*; + +import java.lang.reflect.Type; + +public class ThrowableSerializer implements JsonSerializer, JsonDeserializer +{ + @Override + public JsonElement serialize(Throwable src, Type typeOfSrc, JsonSerializationContext context) + { + JsonObject ob = new JsonObject(); + ob.addProperty("message", src.getMessage()); + ob.add("cause", context.serialize(src.getCause())); + JsonArray stack = new JsonArray(); + for (StackTraceElement trace: src.getStackTrace()) + { + stack.add(context.serialize(trace)); + } + ob.add("trace", stack); + JsonArray suppressed = new JsonArray(); + for (Throwable throwable: src.getSuppressed()) + { + suppressed.add(context.serialize(throwable)); + } + ob.add("suppressed", suppressed); + return ob; + } + + @Override + public Throwable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + JsonObject ob = json.getAsJsonObject(); + Throwable throwable = new Throwable(ob.get("message").getAsString(), context.deserialize(ob.get("cause"), typeOfT)); + throwable.initCause(context.deserialize(ob.get("cause"), Throwable.class)); + JsonArray traceJson = ob.getAsJsonArray("trace"); + StackTraceElement[] trace = new StackTraceElement[traceJson.size()]; + for (int i = 0; i < trace.length; i++) + { + trace[i] = context.deserialize(traceJson.get(i), StackTraceElement.class); + } + throwable.setStackTrace(trace); + JsonArray suppressed = ob.getAsJsonArray("suppressed"); + for (JsonElement el: suppressed) + { + throwable.addSuppressed(context.deserialize(el, Throwable.class)); + } + return throwable; + } +} diff --git a/addonInter/src/main/resources/log4j2.xml b/addonInter/src/main/resources/log4j2.xml index 3aa5a6f..35282cb 100644 --- a/addonInter/src/main/resources/log4j2.xml +++ b/addonInter/src/main/resources/log4j2.xml @@ -18,5 +18,9 @@ + + + + \ No newline at end of file diff --git a/browserCommands/build.gradle b/browserCommands/build.gradle index fa097eb..d2ddb77 100644 --- a/browserCommands/build.gradle +++ b/browserCommands/build.gradle @@ -9,6 +9,9 @@ plugins { // In this section you declare where to find the dependencies of your project repositories { mavenCentral() + flatDir { + dirs new File(rootDir, 'libs') + } } // In this section you declare the dependencies for your production and test code @@ -19,6 +22,8 @@ dependencies { implementation 'org.apache.logging.log4j:log4j-core:2.13.3' implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.13.3' + implementation 'net.harawata:appdirs:1.2.1' + // Declare the dependency for your favourite test framework you want to use in your tests. // TestNG is also supported by the Gradle Test task. Just change the // testCompile dependency to testCompile 'org.testng:testng:6.8.1' and add diff --git a/interface/src/main/java/edu/regis/universeplayer/PlaybackStatus.java b/browserCommands/src/main/java/edu/regis/universeplayer/PlaybackStatus.java similarity index 100% rename from interface/src/main/java/edu/regis/universeplayer/PlaybackStatus.java rename to browserCommands/src/main/java/edu/regis/universeplayer/PlaybackStatus.java diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserConstants.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserConstants.java index ec50ac8..aa463a8 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserConstants.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserConstants.java @@ -4,14 +4,51 @@ package edu.regis.universeplayer.browserCommands; +import net.harawata.appdirs.AppDirsFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; + public class BrowserConstants { + private static final Logger logger = LoggerFactory.getLogger(BrowserConstants.class); /** * The IP that the connection will be hosted on. */ - public static final String IP = "127.0.0.1"; + public static final String IP = "localhost"; /** * The port both processes use for connection. */ public static final int PORT = 3000; + + private static File commDir; + + /** + * Obtains the directory for memory mapped files + * + * @return The communications storage directory. + */ + public static File getCommDir() + { + if (commDir == null) + { + commDir = new File(AppDirsFactory.getInstance().getSharedDir("universalmusic", null, null), "comm"); + if (!commDir.getParentFile().exists()) + { + if (!commDir.getParentFile().mkdir()) + { + logger.error("Could not create shared directory {}", commDir.getParent()); + } + } + if (!commDir.exists()) + { + if (!commDir.mkdir()) + { + logger.error("Could not create data directory {}", commDir); + } + } + } + return commDir; + } } diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandQuit.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandQuit.java new file mode 100644 index 0000000..cd32133 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandQuit.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * The quit command tells the browser to shut down. + * + * @author William Hubbard + * @version 0.1 + */ +public class CommandQuit implements BrowserCommand +{ + public CommandQuit() + { + + } + + @Override + public String getCommandName() + { + return "quit"; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageHandler.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageHandler.java index 0c7f0f5..da2a521 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageHandler.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageHandler.java @@ -19,20 +19,20 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; /** - * + * Dispatches messages sent from a corresponding {@link MessageRunner}. */ public class MessageHandler implements Runnable, MessageSerializer { private final Logger logger; - private final String name; + public final String name; private final InputStream input; private final OutputStream output; private final ExecutorService executor; private final LinkedList listeners = new LinkedList<>(); - private final HashMap> messageResponses = new HashMap<>(); + protected final HashMap> messageResponses = new HashMap<>(); /** * Creates a message handler. @@ -56,14 +56,22 @@ public class MessageHandler implements Runnable, MessageSerializer return logger; } + /** + * Called at the beginning of every loop to do extra processing and check to see if we can still run. + * + * @return True if we should close the runner, false otherwise. + */ + protected boolean onRun() + { + return false; + } + @Override public void run() { BufferedInputStream browserIn = null; BufferedOutputStream browserOut = null; - boolean running = true; - byte[][] message; byte[] messageByte; Object messageOb; @@ -76,7 +84,7 @@ public class MessageHandler implements Runnable, MessageSerializer browserIn = new BufferedInputStream(this.input); browserOut = new BufferedOutputStream(this.output); - while (running) + while (!this.onRun()) { try { @@ -86,7 +94,7 @@ public class MessageHandler implements Runnable, MessageSerializer if (message == null) { logger.info("Connection closed."); - running = false; + break; } else { diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java index 90a7ed2..2ace761 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java @@ -24,25 +24,25 @@ public abstract class MessageRunner implements Runnable, MessageSerializer { private final Logger logger; - private final String name; + public final String name; private final InputStream input; private final OutputStream output; - private final Object readLock = new Object(); + protected final Object readLock = new Object(); /** * This queue serves as a cache for objects we need to send. */ - private final LinkedList sendQueue = new LinkedList<>(); + protected final LinkedList sendQueue = new LinkedList<>(); /** * This queue serves as a cache for objects that are waiting for a response. */ - private final HashMap sentQueue = new HashMap<>(); - private int messagesSent = 0; + protected final HashMap sentQueue = new HashMap<>(); + protected int messagesSent = 0; /** * Creates a message runner. * - * @param name - The name of the runner. This is used in logging. + * @param name - The name of the runner. This is used in logging. * @param input - The input from our external source. * @param output - The output to the external source. */ @@ -60,14 +60,22 @@ public abstract class MessageRunner implements Runnable, MessageSerializer return logger; } + /** + * Called at the beginning of every loop to do extra processing and check to see if we can still run. + * + * @return True if we should close the runner, false otherwise. + */ + protected boolean onRun() + { + return false; + } + @Override public void run() { BufferedInputStream browserIn = null; BufferedOutputStream browserOut = null; MessagePacket packet; - - boolean running = true; byte[][] returnMessage; ByteBuffer numBuffer = ByteBuffer.allocate(4); @@ -77,7 +85,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer browserIn = new BufferedInputStream(this.input); browserOut = new BufferedOutputStream(this.output); - while (running) + while (!this.onRun()) { /* * Sends a messages @@ -121,7 +129,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer if (returnMessage == null) { logger.info("Connection closed."); - running = false; + break; } else { @@ -138,7 +146,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer { packet.returnMessage = returnMessage[1]; numBuffer.clear(); - logger.debug("Reading message {} {}", numBuffer.getInt(), packet.returnMessage); + logger.debug("Reading message {} {}", numBuffer.getInt(), new String(packet.returnMessage, StandardCharsets.UTF_8)); synchronized (this.readLock) { logger.trace("Received message {}, notifying futures.", packet.returnValue.index); @@ -254,9 +262,9 @@ public abstract class MessageRunner implements Runnable, MessageSerializer */ public class MessageFuture implements Future { - private final MessagePacket packetEntry; - private int index; - private boolean canceled = false; + protected final MessagePacket packetEntry; + public int index; + protected boolean canceled = false; private MessageFuture(MessagePacket packet) { diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryStatus.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryStatus.java new file mode 100644 index 0000000..f2e7ecc --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryStatus.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * This query asks for whether a song is currently playing or not. Note that + * this is not the same as whether it is paused or not, as a song could have + * run into a playback error. + * + * @author William Hubbard + * @version 0.1 + */ +public class QueryStatus implements BrowserQuery +{ + @Override + public String getCommandName() + { + return "getStatus"; + } + + @Override + public Class getReturnType() + { + return Integer.class; + } +} diff --git a/interface/build.gradle b/interface/build.gradle index b8c8c49..46d42ad 100644 --- a/interface/build.gradle +++ b/interface/build.gradle @@ -10,6 +10,9 @@ plugins { // In this section you declare where to find the dependencies of your project repositories { mavenCentral() + flatDir { + dirs new File(rootDir, 'libs') + } } // In this section you declare the dependencies for your production and test code diff --git a/interface/src/main/java/edu/regis/universeplayer/browser/Browser.java b/interface/src/main/java/edu/regis/universeplayer/browser/Browser.java index 478962e..649ab93 100644 --- a/interface/src/main/java/edu/regis/universeplayer/browser/Browser.java +++ b/interface/src/main/java/edu/regis/universeplayer/browser/Browser.java @@ -12,11 +12,12 @@ import edu.regis.universeplayer.data.Song; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.File; +import java.io.IOException; import java.net.ConnectException; +import java.net.InetAddress; +import java.net.ServerSocket; import java.net.Socket; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.Scanner; import java.util.concurrent.*; @@ -30,23 +31,19 @@ import java.util.concurrent.*; public class Browser extends MessageRunner implements Player { private static final Logger logger = LoggerFactory.getLogger(Browser.class); - private final Socket socket; private final Process process; + private final Socket socket; + + private final LinkedList listeners = new LinkedList<>(); + + private InternetSong currentSong; public static Browser createBrowser() throws IOException, InterruptedException { - /* - * The maximum number of attempts that will be made to establish a - * connection. - */ - final int MAX_ATTEMPTS = 20; - /* - * How long the thread will sleep between connection attempts, in - * milliseconds. - */ - final long SLEEP_TIME = 500; + ServerSocket server = new ServerSocket(BrowserConstants.PORT, 50, InetAddress.getByName(null)); + logger.debug("Server started."); + int startExit; - Socket socket = null; Browser browser; Process browserProcess = launchBrowser(); /* @@ -69,24 +66,31 @@ public class Browser extends MessageRunner implements Player ConnectException connErr = null; logger.debug("Attempting connection"); - for (int attempts = 0; socket == null && attempts < MAX_ATTEMPTS; attempts++) + Socket socket = server.accept(); + if (!socket.isBound()) { - try - { - socket = new Socket(BrowserConstants.IP, BrowserConstants.PORT); - } - catch (ConnectException e) - { - connErr = e; - logger.debug("Connection attempt {} failed, trying again", attempts); - Thread.sleep(SLEEP_TIME); - } + logger.error("Socket not bound"); } - if (socket == null) + else if (!socket.isConnected()) { - throw connErr; + logger.error("Socket not connected"); + } + else if (socket.isClosed()) + { + logger.error("Socket prematurely closed"); + } + else if (socket.isInputShutdown()) + { + logger.error("Socket input prematurely closed."); + } + else if (socket.isOutputShutdown()) + { + logger.error("Socket input prematurely closed."); + } + else + { + logger.debug("Connection established."); } - logger.debug("Browser connection established."); browser = new Browser(socket, browserProcess); return browser; } @@ -98,6 +102,17 @@ public class Browser extends MessageRunner implements Player this.process = process; } + @Override + protected boolean onRun() + { + if (!socket.isConnected() || socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) + { + logger.debug("Socket closed, shutting down"); + return true; + } + return false; + } + /** * Utility method for launching a browser instance * @@ -155,7 +170,7 @@ public class Browser extends MessageRunner implements Player @Override public Song getCurrentSong() { - return null; + return this.currentSong; } @Override @@ -178,23 +193,28 @@ public class Browser extends MessageRunner implements Player @Override public QueryFuture close() { - if (process != null) - { - logger.info("Destroying browser processes {}, {}", process, process.descendants().toArray(ProcessHandle[]::new)); - process.descendants().forEach(ProcessHandle::destroy); - process.destroy(); - } - else - { - logger.info("Process already destroyed."); - } try { - this.socket.close(); + QueryFuture future = new ForwardedFuture(this.sendObject(new CommandQuit())); + return future; } catch (IOException e) { - logger.error("Could not close browser socket", e); + logger.error("Could not deliver quit command", e); + logger.error("Destroying browser processes {}, {}", process, process.descendants().toArray(ProcessHandle[]::new)); + try + { + socket.close(); + } + catch (IOException ex) + { + logger.error("Could not close socket", ex); + } + finally + { + process.descendants().forEach(ProcessHandle::destroy); + process.destroy(); + } } return null; } @@ -236,19 +256,43 @@ public class Browser extends MessageRunner implements Player @Override public QueryFuture play() { - return null; + try + { + return new ForwardedFuture(this.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY))); + } + catch (IOException e) + { + logger.error("Could not send message", e); + return null; + } } @Override public QueryFuture pause() { - return null; + try + { + return new ForwardedFuture(this.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE))); + } + catch (IOException e) + { + logger.error("Could not send message", e); + return null; + } } @Override public QueryFuture togglePlayback() { - return null; + try + { + return new ForwardedFuture(this.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE))); + } + catch (IOException e) + { + logger.error("Could not send message", e); + return null; + } } /** @@ -263,7 +307,15 @@ public class Browser extends MessageRunner implements Player @Override public QueryFuture seek(float time) { - return null; + try + { + return new ForwardedFuture(this.sendObject(new CommandSeek(time))); + } + catch (IOException e) + { + logger.error("Could not send message", e); + return null; + } } /** @@ -274,7 +326,71 @@ public class Browser extends MessageRunner implements Player @Override public QueryFuture getStatus() { - return null; + try + { + Future future = this.sendObject(new QueryStatus()); + return new QueryFuture<>() { + + private CommandReturn getVal() throws ExecutionException, InterruptedException + { + return ((CommandReturn) future.get()); + } + + private CommandReturn getVal(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException + { + return ((CommandReturn) future.get(timeout, unit)); + } + + @Override + public CommandConfirmation getConfirmation() throws CancellationException, ExecutionException, InterruptedException + { + return this.getVal().getConfirmation(); + } + + @Override + public CommandConfirmation getConfirmation(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException + { + return this.getVal(timeout, unit).getConfirmation(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) + { + return future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() + { + return future.isCancelled(); + } + + @Override + public boolean isDone() + { + return future.isDone(); + } + + @Override + public PlaybackStatus get() throws InterruptedException, ExecutionException + { + Double value = getVal().getReturnValue(); + return PlaybackStatus.values()[value.intValue()]; + } + + @Override + public PlaybackStatus get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException + { + Double value = getVal(timeout, unit).getReturnValue(); + return PlaybackStatus.values()[value.intValue()]; + } + }; + } + catch (IOException e) + { + logger.error("Could not send message", e); + return null; + } } @Override diff --git a/interface/src/main/java/edu/regis/universeplayer/localPlayer/LocalPlayer.java b/interface/src/main/java/edu/regis/universeplayer/localPlayer/LocalPlayer.java index 4e6d18e..0fa7a9a 100644 --- a/interface/src/main/java/edu/regis/universeplayer/localPlayer/LocalPlayer.java +++ b/interface/src/main/java/edu/regis/universeplayer/localPlayer/LocalPlayer.java @@ -4,7 +4,6 @@ package edu.regis.universeplayer.localPlayer; import com.intervigil.wave.WaveReader; - import edu.regis.universeplayer.PlaybackListener; import edu.regis.universeplayer.PlaybackStatus; import edu.regis.universeplayer.Player; @@ -12,6 +11,8 @@ import edu.regis.universeplayer.browserCommands.CommandConfirmation; import edu.regis.universeplayer.browserCommands.QueryFuture; import edu.regis.universeplayer.data.LocalSong; import edu.regis.universeplayer.data.Song; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import uk.co.caprica.vlcj.factory.MediaPlayerFactory; import uk.co.caprica.vlcj.media.MediaRef; import uk.co.caprica.vlcj.media.TrackType; @@ -19,17 +20,12 @@ import uk.co.caprica.vlcj.player.base.MediaPlayer; import uk.co.caprica.vlcj.player.base.MediaPlayerEventListener; import uk.co.caprica.vlcj.player.base.StatusApi; import uk.co.caprica.vlcj.player.component.AudioPlayerComponent; -import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.swing.*; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.util.LinkedList; import java.util.concurrent.*; diff --git a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java index f2f6e11..8411e23 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java @@ -6,6 +6,7 @@ package edu.regis.universeplayer.player; import edu.regis.universeplayer.Player; import edu.regis.universeplayer.browser.Browser; +import edu.regis.universeplayer.browser.InternetSong; import edu.regis.universeplayer.data.Queue; import edu.regis.universeplayer.data.*; import edu.regis.universeplayer.localPlayer.LocalPlayer; @@ -36,6 +37,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL private static final Logger logger = LoggerFactory.getLogger(Interface.class); private static File dataDir; + private static File commDir; private static File configDir; /** @@ -94,29 +96,32 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL inter.players.add(browser = Browser.createBrowser()); browserThread = new Thread(browser); browserThread.start(); + logger.debug("Sending ping"); + browser.sendObject("ping"); Runtime.getRuntime().addShutdownHook(new Thread(browser::close)); + Player.REGISTERED_PLAYERS.put(InternetSong.class, browser); - LinkedList> pingRequests = new LinkedList<>(); - for (int i = 0; i < 20; i++) - { - logger.info("Sending ping"); - pingRequests.add(browser.sendObject("ping")); - } - - LinkedList> toRemove = new LinkedList<>(); - while (pingRequests.size() > 0) - { - for (Future future : pingRequests) - { - if (future.isDone()) - { - logger.info("Receiving {}", future.get()); - toRemove.add(future); - } - } - pingRequests.removeAll(toRemove); - toRemove.clear(); - } +// LinkedList> pingRequests = new LinkedList<>(); +// for (int i = 0; i < 20; i++) +// { +// logger.info("Sending ping"); +// pingRequests.add(browser.sendObject("ping")); +// } +// +// LinkedList> toRemove = new LinkedList<>(); +// while (pingRequests.size() > 0) +// { +// for (Future future : pingRequests) +// { +// if (future.isDone()) +// { +// logger.info("Receiving {}", future.get()); +// toRemove.add(future); +// } +// } +// pingRequests.removeAll(toRemove); +// toRemove.clear(); +// } } catch (IOException e) { @@ -173,6 +178,33 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL return configDir; } + /** + * Obtains the directory for memory mapped files + * @return The communications storage directory. + */ + public static File getCommDir() + { + if (commDir == null) + { + commDir = new File(AppDirsFactory.getInstance().getSharedDir("universalmusic", null, null), "comm"); + if (!commDir.getParentFile().exists()) + { + if (!commDir.getParentFile().mkdir()) + { + logger.error("Could not create shared directory {}", commDir.getParent()); + } + } + if (!commDir.exists()) + { + if (!commDir.mkdir()) + { + logger.error("Could not create data directory {}", commDir); + } + } + } + return commDir; + } + /** * Creates an interface */ diff --git a/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java b/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java index 9d7b9be..01484d9 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/PlayerControls.java @@ -12,6 +12,7 @@ import edu.regis.universeplayer.data.Song; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.swing.*; import java.awt.*; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; @@ -21,8 +22,6 @@ import java.util.LinkedList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; -import javax.swing.*; - /** * This panel contains the buttons necessary for controlling the playback of audio. * diff --git a/interface/src/main/resources/log4j2.xml b/interface/src/main/resources/log4j2.xml index 8a77aee..f450a75 100644 --- a/interface/src/main/resources/log4j2.xml +++ b/interface/src/main/resources/log4j2.xml @@ -20,6 +20,8 @@ + +