diff --git a/libs/limelight-common.jar b/libs/limelight-common.jar index 277bdc4..5b8f846 100644 Binary files a/libs/limelight-common.jar and b/libs/limelight-common.jar differ diff --git a/limelight-pc/icon/Icon Draft/LimeLight.png b/limelight-pc/icon/Icon Draft/LimeLight.png new file mode 100644 index 0000000..a2d39c6 Binary files /dev/null and b/limelight-pc/icon/Icon Draft/LimeLight.png differ diff --git a/limelight-pc/icon/Icon Draft/appstore copy.psd b/limelight-pc/icon/Icon Draft/appstore copy.psd new file mode 100644 index 0000000..c68f77d Binary files /dev/null and b/limelight-pc/icon/Icon Draft/appstore copy.psd differ diff --git a/limelight-pc/icon/Icon Draft/appstore.ai b/limelight-pc/icon/Icon Draft/appstore.ai new file mode 100644 index 0000000..4450607 --- /dev/null +++ b/limelight-pc/icon/Icon Draft/appstore.ai @@ -0,0 +1 @@ +%!PS-Adobe-2.0 %%Creator: Adobe Photoshop(TM) Pen Path Export 7.0 %%Title: (appstore.ai) %%DocumentNeededResources: procset Adobe_packedarray 2.0 0 %%+ procset Adobe_IllustratorA_AI3 1.0 1 %%ColorUsage: Black&White %%BoundingBox: 0 0 600 600 %%HiResBoundingBox: 0 0 600 600 %AI3_Cropmarks: 0 0 600 600 %%DocumentPreview: None %%EndComments %%BeginProlog %%IncludeResource: procset Adobe_packedarray 2.0 0 Adobe_packedarray /initialize get exec %%IncludeResource: procset Adobe_IllustratorA_AI3 1.0 1 %%EndProlog %%BeginSetup Adobe_IllustratorA_AI3 /initialize get exec n %%EndSetup 0.0 0.0 0.0 1.0 k 0 i 0 J 0 j 1 w 4 M []0 d %%Note: %%Trailer %%EOF \ No newline at end of file diff --git a/limelight-pc/icon/Icon Draft/appstore.psd b/limelight-pc/icon/Icon Draft/appstore.psd new file mode 100644 index 0000000..c68f77d Binary files /dev/null and b/limelight-pc/icon/Icon Draft/appstore.psd differ diff --git a/src/com/limelight/Limelight.java b/src/com/limelight/Limelight.java index a881b48..1e2fb4a 100644 --- a/src/com/limelight/Limelight.java +++ b/src/com/limelight/Limelight.java @@ -1,14 +1,12 @@ package com.limelight; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; + import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.UIManager; +import com.limelight.binding.LibraryHelper; import com.limelight.binding.PlatformBinding; import com.limelight.gui.MainFrame; import com.limelight.gui.StreamFrame; @@ -33,7 +31,7 @@ public class Limelight implements NvConnectionListener { private String host; private StreamFrame streamFrame; private NvConnection conn; - private boolean connectionFailed; + private boolean connectionTerminating; private static JFrame limeFrame; /** @@ -189,14 +187,15 @@ public class Limelight implements NvConnectionListener { } } - try { - prepareNativeLibraries(); - } catch (IOException e) { - // This is expected to fail when not in a JAR - } + LibraryHelper.prepareNativeLibraries(); createFrame(); } + + public void stop() { + connectionTerminating = true; + conn.stop(); + } /** * Callback to specify which stage is starting. Used to update UI. @@ -244,9 +243,11 @@ public class Limelight implements NvConnectionListener { */ @Override public void connectionTerminated(Exception e) { - e.printStackTrace(); - if (!connectionFailed) { - connectionFailed = true; + if (!(e instanceof InterruptedException)) { + e.printStackTrace(); + } + if (!connectionTerminating) { + connectionTerminating = true; // Kill the connection to the target conn.stop(); diff --git a/src/com/limelight/binding/LibraryHelper.java b/src/com/limelight/binding/LibraryHelper.java new file mode 100644 index 0000000..0eb7062 --- /dev/null +++ b/src/com/limelight/binding/LibraryHelper.java @@ -0,0 +1,97 @@ +package com.limelight.binding; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; + +public class LibraryHelper { + private static final HashSet avcDependencies = new HashSet(); + private static final boolean needsDependencyExtraction; + private static final String libraryExtractionFolder; + + private static boolean librariesExtracted = false; + + static { + needsDependencyExtraction = System.getProperty("os.name", "").contains("Windows"); + libraryExtractionFolder = System.getProperty("java.io.tmpdir", "."); + + // FFMPEG libraries + avcDependencies.add("avutil-52"); + avcDependencies.add("swresample-0"); + avcDependencies.add("swscale-2"); + avcDependencies.add("avcodec-55"); + avcDependencies.add("avformat-55"); + avcDependencies.add("avfilter-3"); + + // The AVC JNI library itself + avcDependencies.add("nv_avc_dec"); + + // Additional Windows dependencies + if (System.getProperty("os.name").contains("Windows")) { + avcDependencies.add("postproc-52"); + avcDependencies.add("pthreadVC2"); + } + } + + public static void loadNativeLibrary(String libraryName) { + if (librariesExtracted && avcDependencies.contains(libraryName)) { + System.load(libraryExtractionFolder + File.separatorChar + System.mapLibraryName(libraryName)); + } + else { + System.loadLibrary(libraryName); + } + } + + public static void prepareNativeLibraries() { + if (!needsDependencyExtraction) { + return; + } + + try { + for (String dependency : avcDependencies) { + extractNativeLibrary(dependency); + } + } catch (IOException e) { + // This is expected if this code is not running from a JAR + return; + } + + librariesExtracted = true; + } + + private static void extractNativeLibrary(String libraryName) throws IOException { + // convert general library name to platform-specific name + libraryName = System.mapLibraryName(libraryName); + + InputStream resource = new Object().getClass().getResourceAsStream("/binlib/"+libraryName); + if (resource == null) { + throw new FileNotFoundException("Unable to find native library in JAR: "+libraryName); + } + File destination = new File(libraryExtractionFolder+File.separatorChar+libraryName); + + // this will only delete it if it exists, and then create a new file + destination.delete(); + destination.createNewFile(); + + // schedule the temporary file to be deleted when the program exits + destination.deleteOnExit(); + + //this is the janky java 6 way to copy a file + FileOutputStream fos = null; + try { + fos = new FileOutputStream(destination); + int read; + byte[] readBuffer = new byte[16384]; + while ((read = resource.read(readBuffer)) != -1) { + fos.write(readBuffer, 0, read); + } + } finally { + if (fos != null) { + fos.close(); + } + } + } +} diff --git a/src/com/limelight/binding/audio/JavaxAudioRenderer.java b/src/com/limelight/binding/audio/JavaxAudioRenderer.java index b28e3e4..c6106d5 100644 --- a/src/com/limelight/binding/audio/JavaxAudioRenderer.java +++ b/src/com/limelight/binding/audio/JavaxAudioRenderer.java @@ -1,15 +1,13 @@ package com.limelight.binding.audio; -import java.nio.ByteBuffer; - import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; +import com.limelight.nvstream.av.ShortBufferDescriptor; import com.limelight.nvstream.av.audio.AudioRenderer; -import com.limelight.nvstream.av.audio.OpusDecoder; /** * Audio renderer implementation @@ -18,6 +16,15 @@ import com.limelight.nvstream.av.audio.OpusDecoder; public class JavaxAudioRenderer implements AudioRenderer { private SourceDataLine soundLine; + private SoundBuffer soundBuffer; + private byte[] lineBuffer; + private int channelCount; + private int sampleRate; + private boolean reallocateLines; + + public static final int DEFAULT_BUFFER_SIZE = 0; + public static final int STARING_BUFFER_SIZE = 4096; + public static final int STAGING_BUFFERS = 3; // 3 complete frames of audio /** * Takes some audio data and writes it out to the renderer. @@ -28,13 +35,36 @@ public class JavaxAudioRenderer implements AudioRenderer { @Override public void playDecodedAudio(short[] pcmData, int offset, int length) { if (soundLine != null) { - byte[] pcmDataBytes = new byte[length * 2]; - ByteBuffer.wrap(pcmDataBytes).asShortBuffer().put(pcmData, offset, length); - if (soundLine.available() < length) { - soundLine.write(pcmDataBytes, 0, soundLine.available()); + // Queue the decoded samples into the staging sound buffer + soundBuffer.queue(new ShortBufferDescriptor(pcmData, offset, length)); + + int available = soundLine.available(); + if (reallocateLines) { + // Kinda jank. If the queued is larger than available, we are going to have a delay + // so we increase the buffer size + if (available < soundBuffer.size()) { + System.out.println("buffer too full, buffer size: " + soundLine.getBufferSize()); + int currentBuffer = soundLine.getBufferSize(); + soundLine.close(); + createSoundLine(currentBuffer*2); + if (soundLine != null) { + available = soundLine.available(); + System.out.println("creating new line with buffer size: " + soundLine.getBufferSize()); + } + else { + available = 0; + System.out.println("failed to create sound line"); + } + } } - else { - soundLine.write(pcmDataBytes, 0, pcmDataBytes.length); + + // If there's space available in the sound line, pull some data out + // of the staging buffer and write it to the sound line + if (available > 0) { + int written = soundBuffer.fill(lineBuffer, 0, available); + if (written > 0) { + soundLine.write(lineBuffer, 0, written); + } } } } @@ -56,15 +86,51 @@ public class JavaxAudioRenderer implements AudioRenderer { */ @Override public void streamInitialized(int channelCount, int sampleRate) { + + private void createSoundLine(int bufferSize) { AudioFormat audioFormat = new AudioFormat(sampleRate, 16, channelCount, true, true); - DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, OpusDecoder.getMaxOutputShorts()); + + DataLine.Info info; + + if (bufferSize == DEFAULT_BUFFER_SIZE) { + info = new DataLine.Info(SourceDataLine.class, audioFormat); + } + else { + info = new DataLine.Info(SourceDataLine.class, audioFormat, bufferSize); + } + try { soundLine = (SourceDataLine) AudioSystem.getLine(info); - soundLine.open(audioFormat, OpusDecoder.getMaxOutputShorts()*4*2); + + if (bufferSize == DEFAULT_BUFFER_SIZE) { + soundLine.open(audioFormat); + } + else { + soundLine.open(audioFormat, bufferSize); + } + soundLine.start(); + lineBuffer = new byte[soundLine.getBufferSize()]; + soundBuffer = new SoundBuffer(STAGING_BUFFERS); } catch (LineUnavailableException e) { soundLine = null; } } + @Override + public void streamInitialized(int channelCount, int sampleRate) { + this.channelCount = channelCount; + this.sampleRate = sampleRate; + + // Workaround OS X's bad Java mixer + if (System.getProperty("os.name").contains("Mac OS X")) { + createSoundLine(STARING_BUFFER_SIZE); + reallocateLines = true; + } + else { + createSoundLine(DEFAULT_BUFFER_SIZE); + reallocateLines = false; + } + } + } diff --git a/src/com/limelight/binding/audio/SoundBuffer.java b/src/com/limelight/binding/audio/SoundBuffer.java new file mode 100644 index 0000000..8859bad --- /dev/null +++ b/src/com/limelight/binding/audio/SoundBuffer.java @@ -0,0 +1,61 @@ +package com.limelight.binding.audio; + +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; +import java.util.LinkedList; + +import com.limelight.nvstream.av.ShortBufferDescriptor; + +public class SoundBuffer { + + private LinkedList bufferList; + private int maxBuffers; + + public SoundBuffer(int maxBuffers) { + this.bufferList = new LinkedList(); + this.maxBuffers = maxBuffers; + } + + public void queue(ShortBufferDescriptor buff) { + if (bufferList.size() > maxBuffers) { + bufferList.removeFirst(); + } + + bufferList.addLast(buff); + } + + public int size() { + int size = 0; + for (ShortBufferDescriptor desc : bufferList) { + size += desc.length; + } + return size; + } + + public int fill(byte[] data, int offset, int length) { + int filled = 0; + + // Convert offset and length to be relative to shorts + offset /= 2; + length /= 2; + + ShortBuffer sb = ByteBuffer.wrap(data).asShortBuffer(); + sb.position(offset); + while (length > 0 && !bufferList.isEmpty()) { + ShortBufferDescriptor buff = bufferList.getFirst(); + + if (buff.length > length) { + break; + } + + sb.put(buff.data, buff.offset, buff.length); + length -= buff.length; + filled += buff.length; + + bufferList.removeFirst(); + } + + // Return bytes instead of shorts + return filled * 2; + } +} diff --git a/src/com/limelight/binding/video/SwingCpuDecoderRenderer.java b/src/com/limelight/binding/video/SwingCpuDecoderRenderer.java index 8183262..a154045 100644 --- a/src/com/limelight/binding/video/SwingCpuDecoderRenderer.java +++ b/src/com/limelight/binding/video/SwingCpuDecoderRenderer.java @@ -40,23 +40,15 @@ public class SwingCpuDecoderRenderer implements VideoDecoderRenderer { * @param drFlags flags for the decoder and renderer */ @Override - public void setup(int width, int height, Object renderTarget, int drFlags) { - this.targetFps = 30; + public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { + this.targetFps = redrawRate; this.width = width; this.height = height; // Single threaded low latency decode is ideal int avcFlags = AvcDecoder.LOW_LATENCY_DECODE; int threadCount = 1; - - // Hack to work around the bad Java native library loader - // which can't resolve native library dependencies - if (System.getProperty("os.name").contains("Windows")) { - System.loadLibrary("avutil-52"); - System.loadLibrary("postproc-52"); - System.loadLibrary("pthreadVC2"); - } - + int err = AvcDecoder.init(width, height, avcFlags, threadCount); if (err != 0) { throw new IllegalStateException("AVC decoder initialization failure: "+err); diff --git a/src/com/limelight/gui/StreamFrame.java b/src/com/limelight/gui/StreamFrame.java index 8e38481..4d63b4b 100644 --- a/src/com/limelight/gui/StreamFrame.java +++ b/src/com/limelight/gui/StreamFrame.java @@ -11,7 +11,9 @@ import java.awt.GraphicsEnvironment; import java.awt.Point; import java.awt.Toolkit; import java.awt.image.BufferedImage; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import javax.swing.Box; @@ -22,6 +24,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JProgressBar; +import com.limelight.Limelight; import com.limelight.input.KeyboardHandler; import com.limelight.input.MouseHandler; import com.limelight.nvstream.NvConnection; @@ -36,12 +39,17 @@ import com.limelight.nvstream.StreamConfiguration; */ public class StreamFrame extends JFrame { private static final long serialVersionUID = 1L; + + private static final double DESIRED_ASPECT_RATIO = 16.0/9.0; + private static final double ALTERNATE_ASPECT_RATIO = 16.0/10.0; private KeyboardHandler keyboard; private MouseHandler mouse; private JProgressBar spinner; private JLabel spinnerLabel; private Cursor noCursor; + private Limelight limelight; + private NvConnection conn; /** @@ -60,6 +68,8 @@ public class StreamFrame extends JFrame { hideCursor(); } + public void build(Limelight limelight, NvConnection conn, StreamConfiguration streamConfig, boolean fullscreen) { + this.limelight = limelight; /** * Builds the components of this frame with the specified configurations. * @param conn the connection this frame belongs to @@ -85,52 +95,77 @@ public class StreamFrame extends JFrame { this.getRootPane().setBackground(Color.BLACK); if (fullscreen) { - makeFullScreen(); + makeFullScreen(streamConfig); } hideCursor(); this.setVisible(true); } - /* - * Gets the best display mode for the system and desired configuration - */ - private DisplayMode getBestDisplay(DisplayMode[] configs) { - Arrays.sort(configs, new Comparator() { + private ArrayList getDisplayModesByAspectRatio(DisplayMode[] configs, double aspectRatio) { + ArrayList matchingConfigs = new ArrayList(); + + for (DisplayMode config : configs) { + if ((double)config.getWidth()/(double)config.getHeight() == aspectRatio) { + matchingConfigs.add(config); + } + } + + return matchingConfigs; + } + + private DisplayMode getBestDisplay(StreamConfiguration targetConfig, DisplayMode[] configs) { + int targetDisplaySize = targetConfig.getWidth()*targetConfig.getHeight(); + + // Try to match the target aspect ratio + ArrayList aspectMatchingConfigs = getDisplayModesByAspectRatio(configs, DESIRED_ASPECT_RATIO); + if (aspectMatchingConfigs.size() == 0) { + // No matches for the target, so try the alternate + aspectMatchingConfigs = getDisplayModesByAspectRatio(configs, ALTERNATE_ASPECT_RATIO); + if (aspectMatchingConfigs.size() == 0) { + // No matches for either, so just use all of them + aspectMatchingConfigs = new ArrayList(Arrays.asList(configs)); + } + } + + // Sort by display size + Collections.sort(aspectMatchingConfigs, new Comparator() { @Override public int compare(DisplayMode o1, DisplayMode o2) { - if (o1.getWidth() > o2.getWidth()) { + if (o1.getWidth()*o1.getHeight() > o2.getWidth()*o2.getHeight()) { return -1; - } else if (o2.getWidth() > o1.getWidth()) { + } else if (o2.getWidth()*o2.getHeight() > o1.getWidth()*o1.getHeight()) { return 1; } else { return 0; } } }); + + // Find the aspect-matching config with the closest matching display size DisplayMode bestConfig = null; - for (DisplayMode config : configs) { - if (config.getWidth() >= getSize().width && config.getHeight() >= getSize().height) { + for (DisplayMode config : aspectMatchingConfigs) { + if (config.getWidth()*config.getHeight() >= targetDisplaySize) { bestConfig = config; } } - if (bestConfig == null) { - return configs[0]; + + if (bestConfig != null) { + System.out.println("Using full-screen display mode "+bestConfig.getWidth()+"x"+bestConfig.getHeight()+ + " for "+targetConfig.getWidth()+"x"+targetConfig.getHeight()+" stream"); } + return bestConfig; } - /* - * Tries to make the frame fullscreen - */ - private void makeFullScreen() { + private void makeFullScreen(StreamConfiguration streamConfig) { GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); if (gd.isFullScreenSupported()) { this.setUndecorated(true); gd.setFullScreenWindow(this); if (gd.isDisplayChangeSupported()) { - DisplayMode config = getBestDisplay(gd.getDisplayModes()); + DisplayMode config = getBestDisplay(streamConfig, gd.getDisplayModes()); if (config != null) { gd.setDisplayMode(config); } @@ -229,7 +264,7 @@ public class StreamFrame extends JFrame { * Stops the stream and destroys the frame */ public void close() { + limelight.stop(); dispose(); - conn.stop(); } } diff --git a/src/com/limelight/input/gamepad/GamepadListener.java b/src/com/limelight/input/gamepad/GamepadListener.java index 89bfa1b..076513d 100644 --- a/src/com/limelight/input/gamepad/GamepadListener.java +++ b/src/com/limelight/input/gamepad/GamepadListener.java @@ -2,6 +2,8 @@ package com.limelight.input.gamepad; import java.lang.reflect.Constructor; import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; import com.limelight.nvstream.NvConnection; @@ -21,6 +23,9 @@ public class GamepadListener { * @return true if it started a thread, false if the thread is already running. */ public static boolean startUp() { + // Suppress spam from jinput log warnings in DefaultControllerEnvironment + Logger.getLogger(ControllerEnvironment.getDefaultEnvironment().getClass().getName()).setLevel(Level.SEVERE); + if (listenerThread == null || !listenerThread.isAlive()) { System.out.println("Controller Listener thread starting up"); listenerThread = new Thread() { diff --git a/src/com/limelight/nvstream/av/video/cpu/AvcDecoder.java b/src/com/limelight/nvstream/av/video/cpu/AvcDecoder.java new file mode 100644 index 0000000..c6ed5a8 --- /dev/null +++ b/src/com/limelight/nvstream/av/video/cpu/AvcDecoder.java @@ -0,0 +1,50 @@ +package com.limelight.nvstream.av.video.cpu; + +import com.limelight.binding.LibraryHelper; + +public class AvcDecoder { + static { + LibraryHelper.loadNativeLibrary("avutil-52"); + if (System.getProperty("os.name").contains("Windows")) { + LibraryHelper.loadNativeLibrary("postproc-52"); + LibraryHelper.loadNativeLibrary("pthreadVC2"); + } + LibraryHelper.loadNativeLibrary("swresample-0"); + LibraryHelper.loadNativeLibrary("swscale-2"); + LibraryHelper.loadNativeLibrary("avcodec-55"); + LibraryHelper.loadNativeLibrary("avformat-55"); + LibraryHelper.loadNativeLibrary("avfilter-3"); + + LibraryHelper.loadNativeLibrary("nv_avc_dec"); + } + + /** Disables the deblocking filter at the cost of image quality */ + public static final int DISABLE_LOOP_FILTER = 0x1; + /** Uses the low latency decode flag (disables multithreading) */ + public static final int LOW_LATENCY_DECODE = 0x2; + /** Threads process each slice, rather than each frame */ + public static final int SLICE_THREADING = 0x4; + /** Uses nonstandard speedup tricks */ + public static final int FAST_DECODE = 0x8; + /** Uses bilinear filtering instead of bicubic */ + public static final int BILINEAR_FILTERING = 0x10; + /** Uses a faster bilinear filtering with lower image quality */ + public static final int FAST_BILINEAR_FILTERING = 0x20; + /** Disables color conversion (output is NV21) */ + public static final int NO_COLOR_CONVERSION = 0x40; + + public static native int init(int width, int height, int perflvl, int threadcount); + public static native void destroy(); + + // Rendering API when NO_COLOR_CONVERSION == 0 + public static native boolean setRenderTarget(Object androidSurface); + public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize); + public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize); + public static native boolean redraw(); + + // Rendering API when NO_COLOR_CONVERSION == 1 + public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize); + + public static native int getInputPaddingSize(); + public static native int decode(byte[] indata, int inoff, int inlen); +} diff --git a/src/com/limelight/settings/SettingsManager.java b/src/com/limelight/settings/SettingsManager.java index 902f40c..13dcaa1 100644 --- a/src/com/limelight/settings/SettingsManager.java +++ b/src/com/limelight/settings/SettingsManager.java @@ -53,7 +53,7 @@ public class SettingsManager { */ public File getGamepadFile() { if (!settingsDir.exists()) { - settingsFile.mkdirs(); + settingsDir.mkdirs(); } if (!gamepadFile.exists()) { @@ -74,7 +74,7 @@ public class SettingsManager { */ public File getSettingsFile() { if (!settingsDir.exists()) { - settingsFile.mkdirs(); + settingsDir.mkdirs(); } if (!settingsFile.exists()) {