Adds the ability to play youtube videos.

It still needs a bit of work, but playing and pausing are operational.
This commit is contained in:
Markil3
2021-08-16 22:42:44 -06:00
parent b546c81fa1
commit ede9132279
23 changed files with 799 additions and 140 deletions

View File

@@ -1,10 +1,112 @@
console.log("Hello from Universal Music addon!") console.log("Hello from Universal Music addon!")
const tabMessages = new Map();
var numTabMessages = 0;
/* /*
On startup, connect to the "ping_pong" app. On startup, connect to the "ping_pong" app.
*/ */
var port = browser.runtime.connectNative("universalmusic"); 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) { 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) if (returnValue == null)
{ {
returnValue = "pong"; returnValue = "pong";
@@ -12,6 +114,17 @@ var listeners = [function (message, returnValue) {
return 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) { function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
@@ -20,11 +133,25 @@ function handleMessage(message)
{ {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var returnValue = null; 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. * Listen for messages from the app.
*/ */
port.onMessage.addListener((message) => { port.onMessage.addListener((message) => {
console.log("Received: ", message); console.log("Received from interface: ", message);
handleMessage(message.message).then((response) => { handleMessage(message.message).then((response) => {
returnValue = { returnValue = {
"messageNum": message.messageNum, "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) console.log("Sending ", returnValue)
port.postMessage(returnValue); port.postMessage(returnValue);
}).catch(error => {
}); });
}); });
browser.runtime.onConnect.addListener(setupTab);

82
add-on/foreground.js Normal file
View File

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

View File

@@ -22,6 +22,13 @@
"scripts": ["background.js"] "scripts": ["background.js"]
}, },
"content_scripts": [
{
"matches": ["*://*.youtube.com/watch?v=*"],
"js": ["foreground.js", "youtube.js"]
}
],
"browser_action": { "browser_action": {
"default_icon": "icon.svg" "default_icon": "icon.svg"
}, },

76
add-on/youtube.js Normal file
View File

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

View File

@@ -20,6 +20,9 @@ plugins {
// In this section you declare where to find the dependencies of your project // In this section you declare where to find the dependencies of your project
repositories { repositories {
mavenCentral() mavenCentral()
flatDir {
dirs new File(rootDir, 'libs')
}
} }
// In this section you declare the dependencies for your production and test code // 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 'com.google.code.gson:gson:2.8.7'
implementation 'net.harawata:appdirs:1.2.1'
implementation project(":browserCommands") implementation project(":browserCommands")
// Declare the dependency for your favourite test framework you want to use in your tests. // Declare the dependency for your favourite test framework you want to use in your tests.

View File

@@ -27,8 +27,16 @@ import java.util.HashMap;
*/ */
public class BrowserLink extends MessageRunner public class BrowserLink extends MessageRunner
{ {
// private static final Logger logger = LoggerFactory.getLogger(BrowserLink.class); private static final Logger logger = LoggerFactory.getLogger(BrowserLink.class);
public static final Gson gson = new Gson(); 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. * Creates a message runner.
@@ -41,8 +49,12 @@ public class BrowserLink extends MessageRunner
@Override @Override
public byte[] serializeObject(Object message) public byte[] serializeObject(Object message)
{ {
String val = gson.toJson(message); JsonElement val = gson.toJsonTree(message);
return val.getBytes(StandardCharsets.UTF_8); if (val.isJsonObject())
{
((JsonObject) val).addProperty("type", message.getClass().getName());
}
return val.toString().getBytes(StandardCharsets.UTF_8);
} }
@Override @Override
@@ -197,6 +209,7 @@ public class BrowserLink extends MessageRunner
try try
{ {
clazz = Class.forName(message.getAsJsonPrimitive("type").getAsString()); clazz = Class.forName(message.getAsJsonPrimitive("type").getAsString());
message.remove("type");
} }
catch (ClassNotFoundException | ClassCastException e) catch (ClassNotFoundException | ClassCastException e)
{ {

View File

@@ -10,7 +10,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@@ -22,23 +21,33 @@ public class Main
public static void main(String[] args) throws IOException, InterruptedException public static void main(String[] args) throws IOException, InterruptedException
{ {
ServerSocket socketSource = null;
Socket socket = null;
MessageHandler interfaceLink; MessageHandler interfaceLink;
BrowserLink browserLink; BrowserLink browserLink;
Thread interfaceThread, browserThread; Thread interfaceThread, browserThread;
Socket socket;
try try
{ {
logger.debug("Connecting to browser");
browserLink = new BrowserLink("BrowserLink");
try try
{ {
logger.debug("Connecting to browser"); logger.debug("Setting up connection");
browserLink = new BrowserLink("BrowserLink"); socket = new Socket(BrowserConstants.IP,BrowserConstants.PORT);
logger.debug("Setting up server"); logger.debug("Connection established");
socketSource = new ServerSocket(BrowserConstants.PORT);
logger.debug("Server started"); interfaceLink = new MessageHandler("InterfaceHandler", socket.getInputStream(), socket.getOutputStream()) {
socket = socketSource.accept(); @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"); logger.debug("Connection received");
interfaceLink = new MessageHandler("InterfaceHandler", socket.getInputStream(), socket.getOutputStream());
/* /*
* Pretty much just forwards any messages to the browser and * Pretty much just forwards any messages to the browser and
* returns their value. * returns their value.
@@ -64,6 +73,7 @@ public class Main
catch (IOException e) catch (IOException e)
{ {
logger.error("Could not initialize socket", e); logger.error("Could not initialize socket", e);
browserLink.sendObject("quit");
throw e; throw e;
} }
} }
@@ -75,29 +85,6 @@ public class Main
finally finally
{ {
logger.debug("Shutting down interface link"); 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);
}
}
}
} }
} }

View File

@@ -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<StackTraceElement>, JsonDeserializer<StackTraceElement>
{
@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());
}
}

View File

@@ -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<Throwable>, JsonDeserializer<Throwable>
{
@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;
}
}

View File

@@ -18,5 +18,9 @@
<AppenderRef ref="File"/> <AppenderRef ref="File"/>
</Root> </Root>
<Logger name="edu.regis.universeplayer.browserCommands.MessageRunner" level="trace"/> <Logger name="edu.regis.universeplayer.browserCommands.MessageRunner" level="trace"/>
<Logger name="edu.regis.universeplayer.browserCommands.MessageHandler" level="trace"/>
<Logger name="edu.regis.universeplayer.browserCommands.memoryMapping.MemoryRunner" level="trace"/>
<Logger name="edu.regis.universeplayer.browserCommands.memoryMapping.MemoryHandler" level="trace"/>
<Logger name="edu.regis.universeplayer.addon.BrowserLink" level="trace"/>
</Loggers> </Loggers>
</Configuration> </Configuration>

View File

@@ -9,6 +9,9 @@ plugins {
// In this section you declare where to find the dependencies of your project // In this section you declare where to find the dependencies of your project
repositories { repositories {
mavenCentral() mavenCentral()
flatDir {
dirs new File(rootDir, 'libs')
}
} }
// In this section you declare the dependencies for your production and test code // 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-core:2.13.3'
implementation 'org.apache.logging.log4j:log4j-slf4j-impl: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. // 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 // TestNG is also supported by the Gradle Test task. Just change the
// testCompile dependency to testCompile 'org.testng:testng:6.8.1' and add // testCompile dependency to testCompile 'org.testng:testng:6.8.1' and add

View File

@@ -4,14 +4,51 @@
package edu.regis.universeplayer.browserCommands; 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 public class BrowserConstants
{ {
private static final Logger logger = LoggerFactory.getLogger(BrowserConstants.class);
/** /**
* The IP that the connection will be hosted on. * 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. * The port both processes use for connection.
*/ */
public static final int PORT = 3000; 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;
}
} }

View File

@@ -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";
}
}

View File

@@ -19,20 +19,20 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
/** /**
* * Dispatches messages sent from a corresponding {@link MessageRunner}.
*/ */
public class MessageHandler implements Runnable, MessageSerializer public class MessageHandler implements Runnable, MessageSerializer
{ {
private final Logger logger; private final Logger logger;
private final String name; public final String name;
private final InputStream input; private final InputStream input;
private final OutputStream output; private final OutputStream output;
private final ExecutorService executor; private final ExecutorService executor;
private final LinkedList<MessageListener> listeners = new LinkedList<>(); private final LinkedList<MessageListener> listeners = new LinkedList<>();
private final HashMap<Integer, Future<Object>> messageResponses = new HashMap<>(); protected final HashMap<Integer, Future<Object>> messageResponses = new HashMap<>();
/** /**
* Creates a message handler. * Creates a message handler.
@@ -56,14 +56,22 @@ public class MessageHandler implements Runnable, MessageSerializer
return logger; 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 @Override
public void run() public void run()
{ {
BufferedInputStream browserIn = null; BufferedInputStream browserIn = null;
BufferedOutputStream browserOut = null; BufferedOutputStream browserOut = null;
boolean running = true;
byte[][] message; byte[][] message;
byte[] messageByte; byte[] messageByte;
Object messageOb; Object messageOb;
@@ -76,7 +84,7 @@ public class MessageHandler implements Runnable, MessageSerializer
browserIn = new BufferedInputStream(this.input); browserIn = new BufferedInputStream(this.input);
browserOut = new BufferedOutputStream(this.output); browserOut = new BufferedOutputStream(this.output);
while (running) while (!this.onRun())
{ {
try try
{ {
@@ -86,7 +94,7 @@ public class MessageHandler implements Runnable, MessageSerializer
if (message == null) if (message == null)
{ {
logger.info("Connection closed."); logger.info("Connection closed.");
running = false; break;
} }
else else
{ {

View File

@@ -24,25 +24,25 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
{ {
private final Logger logger; private final Logger logger;
private final String name; public final String name;
private final InputStream input; private final InputStream input;
private final OutputStream output; 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. * This queue serves as a cache for objects we need to send.
*/ */
private final LinkedList<MessagePacket> sendQueue = new LinkedList<>(); protected final LinkedList<MessagePacket> sendQueue = new LinkedList<>();
/** /**
* This queue serves as a cache for objects that are waiting for a response. * This queue serves as a cache for objects that are waiting for a response.
*/ */
private final HashMap<Integer, MessagePacket> sentQueue = new HashMap<>(); protected final HashMap<Integer, MessagePacket> sentQueue = new HashMap<>();
private int messagesSent = 0; protected int messagesSent = 0;
/** /**
* Creates a message runner. * 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 input - The input from our external source.
* @param output - The output to the external source. * @param output - The output to the external source.
*/ */
@@ -60,14 +60,22 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
return logger; 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 @Override
public void run() public void run()
{ {
BufferedInputStream browserIn = null; BufferedInputStream browserIn = null;
BufferedOutputStream browserOut = null; BufferedOutputStream browserOut = null;
MessagePacket packet; MessagePacket packet;
boolean running = true;
byte[][] returnMessage; byte[][] returnMessage;
ByteBuffer numBuffer = ByteBuffer.allocate(4); ByteBuffer numBuffer = ByteBuffer.allocate(4);
@@ -77,7 +85,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
browserIn = new BufferedInputStream(this.input); browserIn = new BufferedInputStream(this.input);
browserOut = new BufferedOutputStream(this.output); browserOut = new BufferedOutputStream(this.output);
while (running) while (!this.onRun())
{ {
/* /*
* Sends a messages * Sends a messages
@@ -121,7 +129,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
if (returnMessage == null) if (returnMessage == null)
{ {
logger.info("Connection closed."); logger.info("Connection closed.");
running = false; break;
} }
else else
{ {
@@ -138,7 +146,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
{ {
packet.returnMessage = returnMessage[1]; packet.returnMessage = returnMessage[1];
numBuffer.clear(); 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) synchronized (this.readLock)
{ {
logger.trace("Received message {}, notifying futures.", packet.returnValue.index); 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<Object> public class MessageFuture implements Future<Object>
{ {
private final MessagePacket packetEntry; protected final MessagePacket packetEntry;
private int index; public int index;
private boolean canceled = false; protected boolean canceled = false;
private MessageFuture(MessagePacket packet) private MessageFuture(MessagePacket packet)
{ {

View File

@@ -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<Integer>
{
@Override
public String getCommandName()
{
return "getStatus";
}
@Override
public Class<Integer> getReturnType()
{
return Integer.class;
}
}

View File

@@ -10,6 +10,9 @@ plugins {
// In this section you declare where to find the dependencies of your project // In this section you declare where to find the dependencies of your project
repositories { repositories {
mavenCentral() mavenCentral()
flatDir {
dirs new File(rootDir, 'libs')
}
} }
// In this section you declare the dependencies for your production and test code // In this section you declare the dependencies for your production and test code

View File

@@ -12,11 +12,12 @@ import edu.regis.universeplayer.data.Song;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.*; import java.io.File;
import java.io.IOException;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.*; import java.util.concurrent.*;
@@ -30,23 +31,19 @@ import java.util.concurrent.*;
public class Browser extends MessageRunner implements Player<InternetSong> public class Browser extends MessageRunner implements Player<InternetSong>
{ {
private static final Logger logger = LoggerFactory.getLogger(Browser.class); private static final Logger logger = LoggerFactory.getLogger(Browser.class);
private final Socket socket;
private final Process process; private final Process process;
private final Socket socket;
private final LinkedList<PlaybackListener> listeners = new LinkedList<>();
private InternetSong currentSong;
public static Browser createBrowser() throws IOException, InterruptedException public static Browser createBrowser() throws IOException, InterruptedException
{ {
/* ServerSocket server = new ServerSocket(BrowserConstants.PORT, 50, InetAddress.getByName(null));
* The maximum number of attempts that will be made to establish a logger.debug("Server started.");
* connection.
*/
final int MAX_ATTEMPTS = 20;
/*
* How long the thread will sleep between connection attempts, in
* milliseconds.
*/
final long SLEEP_TIME = 500;
int startExit; int startExit;
Socket socket = null;
Browser browser; Browser browser;
Process browserProcess = launchBrowser(); Process browserProcess = launchBrowser();
/* /*
@@ -69,24 +66,31 @@ public class Browser extends MessageRunner implements Player<InternetSong>
ConnectException connErr = null; ConnectException connErr = null;
logger.debug("Attempting connection"); logger.debug("Attempting connection");
for (int attempts = 0; socket == null && attempts < MAX_ATTEMPTS; attempts++) Socket socket = server.accept();
if (!socket.isBound())
{ {
try logger.error("Socket not bound");
{
socket = new Socket(BrowserConstants.IP, BrowserConstants.PORT);
}
catch (ConnectException e)
{
connErr = e;
logger.debug("Connection attempt {} failed, trying again", attempts);
Thread.sleep(SLEEP_TIME);
}
} }
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); browser = new Browser(socket, browserProcess);
return browser; return browser;
} }
@@ -98,6 +102,17 @@ public class Browser extends MessageRunner implements Player<InternetSong>
this.process = process; 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 * Utility method for launching a browser instance
* *
@@ -155,7 +170,7 @@ public class Browser extends MessageRunner implements Player<InternetSong>
@Override @Override
public Song getCurrentSong() public Song getCurrentSong()
{ {
return null; return this.currentSong;
} }
@Override @Override
@@ -178,23 +193,28 @@ public class Browser extends MessageRunner implements Player<InternetSong>
@Override @Override
public QueryFuture<Void> close() public QueryFuture<Void> 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 try
{ {
this.socket.close(); QueryFuture<Void> future = new ForwardedFuture(this.sendObject(new CommandQuit()));
return future;
} }
catch (IOException e) 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; return null;
} }
@@ -236,19 +256,43 @@ public class Browser extends MessageRunner implements Player<InternetSong>
@Override @Override
public QueryFuture<Void> play() public QueryFuture<Void> 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 @Override
public QueryFuture<Void> pause() public QueryFuture<Void> 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 @Override
public QueryFuture<Void> togglePlayback() public QueryFuture<Void> 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<InternetSong>
@Override @Override
public QueryFuture<Void> seek(float time) public QueryFuture<Void> 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<InternetSong>
@Override @Override
public QueryFuture<PlaybackStatus> getStatus() public QueryFuture<PlaybackStatus> getStatus()
{ {
return null; try
{
Future future = this.sendObject(new QueryStatus());
return new QueryFuture<>() {
private CommandReturn<Double> getVal() throws ExecutionException, InterruptedException
{
return ((CommandReturn<Double>) future.get());
}
private CommandReturn<Double> getVal(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException
{
return ((CommandReturn<Double>) 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 @Override

View File

@@ -4,7 +4,6 @@
package edu.regis.universeplayer.localPlayer; package edu.regis.universeplayer.localPlayer;
import com.intervigil.wave.WaveReader; import com.intervigil.wave.WaveReader;
import edu.regis.universeplayer.PlaybackListener; import edu.regis.universeplayer.PlaybackListener;
import edu.regis.universeplayer.PlaybackStatus; import edu.regis.universeplayer.PlaybackStatus;
import edu.regis.universeplayer.Player; 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.browserCommands.QueryFuture;
import edu.regis.universeplayer.data.LocalSong; import edu.regis.universeplayer.data.LocalSong;
import edu.regis.universeplayer.data.Song; 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.factory.MediaPlayerFactory;
import uk.co.caprica.vlcj.media.MediaRef; import uk.co.caprica.vlcj.media.MediaRef;
import uk.co.caprica.vlcj.media.TrackType; 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.MediaPlayerEventListener;
import uk.co.caprica.vlcj.player.base.StatusApi; import uk.co.caprica.vlcj.player.base.StatusApi;
import uk.co.caprica.vlcj.player.component.AudioPlayerComponent; 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 javax.swing.*;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.concurrent.*; import java.util.concurrent.*;

View File

@@ -6,6 +6,7 @@ package edu.regis.universeplayer.player;
import edu.regis.universeplayer.Player; import edu.regis.universeplayer.Player;
import edu.regis.universeplayer.browser.Browser; import edu.regis.universeplayer.browser.Browser;
import edu.regis.universeplayer.browser.InternetSong;
import edu.regis.universeplayer.data.Queue; import edu.regis.universeplayer.data.Queue;
import edu.regis.universeplayer.data.*; import edu.regis.universeplayer.data.*;
import edu.regis.universeplayer.localPlayer.LocalPlayer; 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 final Logger logger = LoggerFactory.getLogger(Interface.class);
private static File dataDir; private static File dataDir;
private static File commDir;
private static File configDir; private static File configDir;
/** /**
@@ -94,29 +96,32 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
inter.players.add(browser = Browser.createBrowser()); inter.players.add(browser = Browser.createBrowser());
browserThread = new Thread(browser); browserThread = new Thread(browser);
browserThread.start(); browserThread.start();
logger.debug("Sending ping");
browser.sendObject("ping");
Runtime.getRuntime().addShutdownHook(new Thread(browser::close)); Runtime.getRuntime().addShutdownHook(new Thread(browser::close));
Player.REGISTERED_PLAYERS.put(InternetSong.class, browser);
LinkedList<Future<Object>> pingRequests = new LinkedList<>(); // LinkedList<Future<Object>> pingRequests = new LinkedList<>();
for (int i = 0; i < 20; i++) // for (int i = 0; i < 20; i++)
{ // {
logger.info("Sending ping"); // logger.info("Sending ping");
pingRequests.add(browser.sendObject("ping")); // pingRequests.add(browser.sendObject("ping"));
} // }
//
LinkedList<Future<Object>> toRemove = new LinkedList<>(); // LinkedList<Future<Object>> toRemove = new LinkedList<>();
while (pingRequests.size() > 0) // while (pingRequests.size() > 0)
{ // {
for (Future<Object> future : pingRequests) // for (Future<Object> future : pingRequests)
{ // {
if (future.isDone()) // if (future.isDone())
{ // {
logger.info("Receiving {}", future.get()); // logger.info("Receiving {}", future.get());
toRemove.add(future); // toRemove.add(future);
} // }
} // }
pingRequests.removeAll(toRemove); // pingRequests.removeAll(toRemove);
toRemove.clear(); // toRemove.clear();
} // }
} }
catch (IOException e) catch (IOException e)
{ {
@@ -173,6 +178,33 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
return configDir; 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 * Creates an interface
*/ */

View File

@@ -12,6 +12,7 @@ import edu.regis.universeplayer.data.Song;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.FocusAdapter; import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent; import java.awt.event.FocusEvent;
@@ -21,8 +22,6 @@ import java.util.LinkedList;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool;
import javax.swing.*;
/** /**
* This panel contains the buttons necessary for controlling the playback of audio. * This panel contains the buttons necessary for controlling the playback of audio.
* *

View File

@@ -20,6 +20,8 @@
<Logger name="edu.regis.universeplayer.data.LocalSongProvider" level="debug"> <Logger name="edu.regis.universeplayer.data.LocalSongProvider" level="debug">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
</Logger> </Logger>
<Logger name="edu.regis.universeplayer.browserCommands.memoryMapping.MemoryRunner" level="trace"/>
<Logger name="edu.regis.universeplayer.browserCommands.memoryMapping.MemoryHandler" level="trace"/>
<!-- <Logger name="edu.regis.universeplayer.localPlayer.LocalPlayer" level="debug">--> <!-- <Logger name="edu.regis.universeplayer.localPlayer.LocalPlayer" level="debug">-->
<!-- <AppenderRef ref="Console"/>--> <!-- <AppenderRef ref="Console"/>-->
<!-- </Logger>--> <!-- </Logger>-->