package com.limelight;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import com.limelight.binding.LibraryHelper;
import com.limelight.binding.PlatformBinding;
import com.limelight.gui.MainFrame;
import com.limelight.gui.StreamFrame;
import com.limelight.input.gamepad.Gamepad;
import com.limelight.input.gamepad.GamepadListener;
import com.limelight.input.gamepad.NativeGamepad;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.settings.PreferencesManager;
import com.limelight.settings.SettingsManager;
import com.limelight.settings.PreferencesManager.Preferences;
import com.limelight.settings.PreferencesManager.Preferences.Resolution;
/**
* Main class for Limelight-pc contains methods for starting the application as well
* as the stream to the host pc.
* @author Diego Waxemberg<br>
* Cameron Gutman
*/
public class Limelight implements NvConnectionListener {
public static final double VERSION = 1.0;
public static boolean COMMAND_LINE_LAUNCH = false;
private String host;
private StreamFrame streamFrame;
private NvConnection conn;
private boolean connectionTerminating;
private static JFrame limeFrame;
private Gamepad gamepad;
private VideoDecoderRenderer decoderRenderer;
/**
* Constructs a new instance based on the given host
* @param host can be hostname or IP address.
*/
public Limelight(String host) {
this.host = host;
}
/*
* Creates a connection to the host and starts up the stream.
*/
private void startUp(StreamConfiguration streamConfig, Preferences prefs) {
streamFrame = new StreamFrame();
decoderRenderer = PlatformBinding.getVideoDecoderRenderer();
conn = new NvConnection(host, prefs.getUniqueId(), this, streamConfig, PlatformBinding.getCryptoProvider());
streamFrame.build(this, conn, streamConfig, prefs.getFullscreen());
conn.start(PlatformBinding.getDeviceName(), streamFrame,
VideoDecoderRenderer.FLAG_PREFER_QUALITY,
PlatformBinding.getAudioRenderer(),
decoderRenderer);
}
/*
* Creates a StreamConfiguration given a Resolution.
* Used to specify what kind of stream will be used.
*/
private static StreamConfiguration createConfiguration(Resolution res, Integer bitRate) {
return new StreamConfiguration.Builder()
.setApp("Steam")
.setResolution(res.width, res.height)
.setRefreshRate(res.frameRate)
.setBitrate(bitRate*1000).build();
}
/*
* Creates the main frame for the application.
*/
private static void createFrame() {
// Tell the user how to map the gamepad if it's a new install and there's no default for this platform
if (!PreferencesManager.hasExistingPreferences() &&
!System.getProperty("os.name").contains("Windows")) {
JOptionPane.showMessageDialog(null, "Gamepad mapping is not set. If you want to use a gamepad, "+
"click the Options menu and choose Gamepad Settings. After mapping your gamepad,"+
" it will work while streaming.", "Limelight", JOptionPane.INFORMATION_MESSAGE);
}
MainFrame main = new MainFrame();
main.build();
limeFrame = main.getLimeFrame();
}
/**
* Load native libraries for this platform or show an error dialog
* @return Error message or null for success
*/
public static String loadNativeLibraries() {
String errorMessage;
try {
String libraryPlatform = LibraryHelper.getLibraryPlatformString();
String jrePlatform = LibraryHelper.getRunningPlatformString();
if (libraryPlatform.equals(jrePlatform)) {
// Success path
LibraryHelper.prepareNativeLibraries();
NativeGamepad.addListener(GamepadListener.getInstance());
NativeGamepad.start();
return null;
}
else {
errorMessage = "This is not the correct JAR for your platform. Please download the \""+jrePlatform+"\" JAR.";
}
} catch (IOException e) {
errorMessage = "The JAR is malformed or an invalid native library path was specified.";
}
return errorMessage;
}
/**
* Creates a new instance and starts the stream.
* @param host the host pc to connect to. Can be a hostname or IP address.
*/
public static void createInstance(String host) {
Limelight limelight = new Limelight(host);
Preferences prefs = PreferencesManager.getPreferences();
StreamConfiguration streamConfig = createConfiguration(prefs.getResolution(), prefs.getBitrate());
limelight.startUp(streamConfig, prefs);
}
/**
* The entry point for the application. <br>
* Does some initializations and then creates the main frame.
* @param args unused.
*/
public static void main(String args[]) {
// Redirect logging to a file if we're running from a JAR
if (LibraryHelper.isRunningFromJar() && args.length == 0) {
try {
LimeLog.setFileHandler(SettingsManager.SETTINGS_DIR + File.separator + "limelight.log");
} catch (IOException e) {
}
}
//fix the menu bar if we are running in osx
if (System.getProperty("os.name").contains("Mac OS X")) {
// set the name of the application menu item
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Limelight");
}
GamepadListener.getInstance().addDeviceListener(new Gamepad());
String libraryError = loadNativeLibraries();
// launching with command line arguments
if (args.length > 0) {
if (libraryError == null) {
parseCommandLine(args);
}
else {
// Print the error to stderr if running from command line
System.err.println(libraryError);
}
} else {
if (libraryError == null) {
createFrame();
} else {
JOptionPane.showMessageDialog(null, libraryError, "Wrong JAR platform", JOptionPane.ERROR_MESSAGE);
}
}
}
//TODO: make this less jank
private static void parseCommandLine(String[] args) {
String host = null;
boolean fullscreen = false;
int resolution = 720;
int refresh = 60;
Integer bitrate = null;
Preferences prefs = PreferencesManager.getPreferences();
// Save preferences to preserve possibly new unique ID
PreferencesManager.writePreferences(prefs);
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-pair")) {
if (i + 1 < args.length){
host = args[i+1];
System.out.println("Trying to pair to: " + host);
String msg = pair(prefs.getUniqueId(), host);
System.out.println("Pairing: " + msg);
System.exit(0);
} else {
System.err.println("Syntax error: hostname or ip address expected after -pair");
System.exit(4);
}
} else if (args[i].equals("-host")) {
if (i + 1 < args.length) {
host = args[i+1];
i++;
} else {
System.err.println("Syntax error: hostname or ip address expected after -host");
System.exit(3);
}
} else if (args[i].equals("-bitrate")) {
if (i + 1 < args.length){
bitrate = Integer.parseInt(args[i+1]);
i++;
} else {
System.err.println("Syntax error: bitrate (in Mbps) expected after -bitrate");
System.exit(3);
}
} else if (args[i].equals("-fs")) {
fullscreen = true;
} else if (args[i].equals("-720")) {
resolution = 720;
} else if (args[i].equals("-768")) {
resolution = 768;
} else if (args[i].equals("-900")) {
resolution = 900;
} else if (args[i].equals("-1080")) {
resolution = 1080;
} else if (args[i].equals("-30fps")) {
refresh = 30;
} else if (args[i].equals("-60fps")) {
refresh = 60;
} else {
System.out.println("Syntax Error: Unrecognized argument: " + args[i]);
}
}
if (host == null) {
System.out.println("Syntax Error: You must include a host. Use -host to specifiy a hostname or ip address.");
System.exit(5);
}
Resolution streamRes = Resolution.findRes(resolution, refresh);
if (bitrate == null) {
bitrate = streamRes.defaultBitrate;
}
StreamConfiguration streamConfig = createConfiguration(streamRes, bitrate);
prefs.setResolution(streamRes);
prefs.setBitrate(bitrate);
prefs.setFullscreen(fullscreen);
Limelight limelight = new Limelight(host);
limelight.startUp(streamConfig, prefs);
COMMAND_LINE_LAUNCH = true;
}
public void stop() {
connectionTerminating = true;
// Kill the connection to the target
conn.stop();
// Remove the gamepad listener
if (gamepad != null) {
GamepadListener.getInstance().removeListener(gamepad);
}
int endToEndLatency = decoderRenderer.getAverageEndToEndLatency();
if (endToEndLatency != 0) {
displayMessage(String.format("Average client latency: %d ms",
endToEndLatency));
}
// Close the stream frame
streamFrame.dispose();
}
/**
* Callback to specify which stage is starting. Used to update UI.
* @param stage the Stage that is starting
*/
public void stageStarting(Stage stage) {
LimeLog.info("Starting "+stage.getName());
streamFrame.showSpinner(stage);
}
/**
* Callback that a stage has finished loading.
* <br><b>NOTE: Currently unimplemented.</b>
* @param stage the Stage that has finished.
*/
public void stageComplete(Stage stage) {
}
/**
* Callback that a stage has failed. Used to inform user that an error occurred.
* @param stage the Stage that was loading when the error occurred
*/
public void stageFailed(Stage stage) {
stop();
displayError("Connection Error", "Starting " + stage.getName() + " failed");
}
/**
* Callback that the connection has finished loading and is started.
*/
public void connectionStarted() {
streamFrame.hideSpinner();
gamepad = new Gamepad(conn);
GamepadListener.getInstance().addDeviceListener(gamepad);
}
/**
* Callback that the connection has been terminated for some reason.
* <br>This is were the stream shutdown procedure takes place.
* @param e the Exception that was thrown- probable cause of termination.
*/
public void connectionTerminated(Exception e) {
if (!(e instanceof InterruptedException)) {
e.printStackTrace();
}
if (!connectionTerminating) {
stop();
// Spin off a new thread to update the UI since
// this thread has been interrupted and will terminate
// shortly
new Thread(new Runnable() {
public void run() {
displayError("Connection Terminated", "The connection failed unexpectedly");
}
}).start();
}
}
public static String pair(final String uniqueId, final String host) {
String message = "";
NvHTTP httpConn;
try {
httpConn = new NvHTTP(InetAddress.getByName(host),
uniqueId, PlatformBinding.getDeviceName(), PlatformBinding.getCryptoProvider());
try {
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
message = "Already paired";
}
else {
final String pinStr = PairingManager.generatePinString();
// Spin the dialog off in a thread because it blocks
new Thread(new Runnable() {
public void run() {
JOptionPane.showMessageDialog(null, "Please enter the following PIN on the target PC: "+pinStr,
"Limelight", JOptionPane.INFORMATION_MESSAGE);
}
}).start();
PairingManager.PairState pairState = httpConn.pair(pinStr);
if (pairState == PairingManager.PairState.PIN_WRONG) {
message = "Incorrect PIN";
}
else if (pairState == PairingManager.PairState.FAILED) {
message = "Pairing failed";
}
else if (pairState == PairingManager.PairState.PAIRED) {
message = "Paired successfully";
}
}
} catch (Exception e) {
message = e.getMessage();
e.printStackTrace();
}
} catch (UnknownHostException e1) {
message = "Failed to resolve host";
}
// Dismiss the pairing dialog before showing the final status dialog
JOptionPane.getRootFrame().dispose();
return message;
}
/**
* Displays a message to the user in the form of an info dialog.
* @param message the message to show the user
*/
public void displayMessage(String message) {
streamFrame.dispose();
JOptionPane.showMessageDialog(limeFrame, message, "Limelight", JOptionPane.INFORMATION_MESSAGE);
}
/**
* Displays an error to the user in the form of an error dialog
* @param title the title for the dialog frame
* @param message the message to show the user
*/
public void displayError(String title, String message) {
streamFrame.dispose();
JOptionPane.showMessageDialog(limeFrame, message, title, JOptionPane.ERROR_MESSAGE);
}
public void displayTransientMessage(String message) {
// FIXME: Implement transient messages
}
}