diff --git a/app/app.iml b/app/app.iml index 1c289387..539222d2 100644 --- a/app/app.iml +++ b/app/app.iml @@ -102,17 +102,17 @@ - + - + - + diff --git a/app/build.gradle b/app/build.gradle index d17049e4..f3f1e5b7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 16 targetSdkVersion 21 - versionName "3.0" - versionCode = 46 + versionName "3.0.1" + versionCode = 47 } productFlavors { @@ -62,13 +62,19 @@ android { } dependencies { - compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9' - compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' - compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' - compile group: 'com.google.android', name: 'support-v4', version:'r7' - compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.1' - compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.1.0' - compile group: 'com.squareup.okio', name:'okio', version:'1.0.1' + compile group: 'org.jcodec', name: 'jcodec', version: '+' + compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+' + compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+' + compile group: 'com.google.android', name: 'support-v4', version:'+' + + // FIXME: Pending resolution of issue #346 using custom build + //compile group: 'com.koushikdutta.ion', name: 'ion', version:'+' + compile group: 'com.google.code.gson', name: 'gson', version:'+' + compile files('libs/androidasync-e1dfb4.jar') + compile files('libs/ion-2f46fa.jar') + + compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+' + compile group: 'com.squareup.okio', name:'okio', version:'+' compile files('libs/jmdns-fixed.jar') compile files('libs/limelight-common.jar') compile files('libs/tinyrtsp.jar') diff --git a/app/libs/androidasync-e1dfb4.jar b/app/libs/androidasync-e1dfb4.jar new file mode 100644 index 00000000..a583e4d5 Binary files /dev/null and b/app/libs/androidasync-e1dfb4.jar differ diff --git a/app/libs/ion-2f46fa.jar b/app/libs/ion-2f46fa.jar new file mode 100644 index 00000000..08fbea63 Binary files /dev/null and b/app/libs/ion-2f46fa.jar differ diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index a3300a52..c5906df8 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -178,7 +178,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, .enableAdaptiveResolution((decoderRenderer.getCapabilities() & VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0) .enableLocalAudioPlayback(prefConfig.playHostAudio) - .setMaxPacketSize(remote ? 1024 : 1392) + .setMaxPacketSize(remote ? 1024 : 1292) .build(); // Initialize the connection diff --git a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java index 10c88a25..e68b475d 100644 --- a/app/src/main/java/com/limelight/binding/input/ControllerHandler.java +++ b/app/src/main/java/com/limelight/binding/input/ControllerHandler.java @@ -47,6 +47,7 @@ public class ControllerHandler { private NvConnection conn; private double stickDeadzone; private final ControllerMapping defaultMapping = new ControllerMapping(); + private boolean hasGameController; public ControllerHandler(NvConnection conn, int deadzonePercentage) { this.conn = conn; @@ -55,6 +56,20 @@ public class ControllerHandler { // is required for controller batching support to work. deadzonePercentage = 10; + int[] ids = InputDevice.getDeviceIds(); + for (int i = 0; i < ids.length; i++) { + InputDevice dev = InputDevice.getDevice(ids[i]); + if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 || + (dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) { + // This looks like a gamepad, but we'll check X and Y to be sure + if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) != null && + getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) != null) { + // This is a gamepad + hasGameController = true; + } + } + } + // 1% is the lowest possible deadzone we support if (deadzonePercentage <= 0) { deadzonePercentage = 1; @@ -94,6 +109,12 @@ public class ControllerHandler { mapping.leftStickXAxis = MotionEvent.AXIS_X; mapping.leftStickYAxis = MotionEvent.AXIS_Y; + if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null && + getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) { + // This is a gamepad + hasGameController = true; + mapping.hasJoystickAxes = true; + } InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER); InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER); @@ -182,27 +203,43 @@ public class ControllerHandler { // It's important to have a valid deadzone so controller packet batching works properly mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat())); - // For triggers without (valid) deadzones, we'll use 10% - if (mapping.triggerDeadzone <= 0.02 || - mapping.triggerDeadzone > 0.30) + // For triggers without (valid) deadzones, we'll use 13% (around XInput's default) + if (mapping.triggerDeadzone < 0.13f || + mapping.triggerDeadzone > 0.30f) { - mapping.triggerDeadzone = 0.1f; + mapping.triggerDeadzone = 0.13f; } } - // For the Nexus Player (and probably other ATV devices), we should - // use the back button as start since it doesn't have a start/menu button - // on the controller - if (devName != null && devName.contains("ASUS Gamepad")) { - // We can only do this check on KitKat or higher, but it doesn't matter since ATV - // is Android 5.0 anyway - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { - boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0); - if (!hasStartKey[0] && !hasStartKey[1]) { - mapping.backIsStart = true; + if (devName != null) { + // For the Nexus Player (and probably other ATV devices), we should + // use the back button as start since it doesn't have a start/menu button + // on the controller + if (devName.contains("ASUS Gamepad")) { + // We can only do this check on KitKat or higher, but it doesn't matter since ATV + // is Android 5.0 anyway + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0); + if (!hasStartKey[0] && !hasStartKey[1]) { + mapping.backIsStart = true; + } + } + + // The ASUS Gamepad has triggers that sit far forward and are prone to false presses + // so we increase the deadzone on them to minimize this + mapping.triggerDeadzone = 0.30f; + } + // Classify this device as a remote by name + else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) { + // It's only a remote if it doesn't any sticks + if (!mapping.hasJoystickAxes) { + mapping.isRemote = true; } } } + + LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius); + LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone); return mapping; } @@ -232,9 +269,18 @@ public class ControllerHandler { conn.sendControllerInput(inputMap, leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY); } - - private static int handleRemapping(ControllerMapping mapping, KeyEvent event) { - if (mapping.isDualShock4) { + + // Return a valid keycode, 0 to consume, or -1 to not consume the event + // Device MAY BE NULL + private int handleRemapping(ControllerMapping mapping, KeyEvent event) { + // For remotes, don't capture the back button + if (mapping.isRemote) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + return -1; + } + } + + if (mapping.isDualShock4) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_BUTTON_Y: return KeyEvent.KEYCODE_BUTTON_L1; @@ -718,5 +764,7 @@ public class ControllerHandler { public boolean isDualShock4; public boolean isXboxController; public boolean backIsStart; + public boolean isRemote; + public boolean hasJoystickAxes; } } diff --git a/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java index 7b05b341..c43fd930 100644 --- a/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/AndroidCpuDecoderRenderer.java @@ -141,7 +141,9 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { throw new IllegalStateException("AVC decoder initialization failure: "+err); } - AvcDecoder.setRenderTarget(sh.getSurface()); + if (!AvcDecoder.setRenderTarget(sh.getSurface())) { + return false; + } decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize()); @@ -252,7 +254,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { // Add delta time to the totals (excluding probable outliers) long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp(); - if (delta >= 0 && delta < 300) { + if (delta >= 0 && delta < 1000) { totalTimeMs += delta; totalFrames++; } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 02e5ce36..5af027e0 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -273,7 +273,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { // Add delta time to the totals (excluding probable outliers) long delta = System.currentTimeMillis()-(presentationTimeUs/1000); - if (delta >= 0 && delta < 300) { + if (delta >= 0 && delta < 1000) { decoderTimeMs += delta; totalTimeMs += delta; } @@ -371,7 +371,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) { long currentTime = System.currentTimeMillis(); long delta = currentTime-decodeUnit.getReceiveTimestamp(); - if (delta >= 0 && delta < 300) { + if (delta >= 0 && delta < 1000) { totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp(); totalFrames++; } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index a101a2b2..e106271e 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -44,6 +44,7 @@ public class MediaCodecHelper { spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk"); + spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm"); baselineProfileHackPrefixes = new LinkedList(); baselineProfileHackPrefixes.add("omx.intel"); diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index da125682..182ff690 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -244,9 +244,12 @@ public class ComputerManagerService extends Service { for (PollingTuple tuple : pollingTuples) { // Check if this is the same computer if (tuple.computer == details || - tuple.computer.localIp.equals(details.localIp) || - tuple.computer.remoteIp.equals(details.remoteIp) || - tuple.computer.name.equals(details.name)) { + // If there's no name on one of these computers, compare with the local IP + ((details.name.isEmpty() || tuple.computer.name.isEmpty()) && + tuple.computer.localIp.equals(details.localIp)) || + // If there is a name on both computers, compare with name + ((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) && + tuple.computer.name.equals(details.name))) { // Start a polling thread if polling is active if (pollingActive && tuple.thread == null) { @@ -329,12 +332,22 @@ public class ComputerManagerService extends Service { } } - private ComputerDetails tryPollIp(InetAddress ipAddr) { + private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) { try { NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(), null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); - return http.getComputerDetails(); + ComputerDetails newDetails = http.getComputerDetails(); + + // Check if this is the PC we expected + if (details.uuid != null && newDetails.uuid != null && + !details.uuid.equals(newDetails.uuid)) { + // We got the wrong PC! + LimeLog.info("Polling returned the wrong PC!"); + return null; + } + + return newDetails; } catch (Exception e) { return null; } @@ -342,21 +355,27 @@ public class ComputerManagerService extends Service { private boolean pollComputer(ComputerDetails details, boolean localFirst) { ComputerDetails polledDetails; + + // If the local address is routable across the Internet, + // always consider this PC remote to be conservative + if (details.localIp.equals(details.remoteIp)) { + localFirst = false; + } if (localFirst) { - polledDetails = tryPollIp(details.localIp); + polledDetails = tryPollIp(details, details.localIp); } else { - polledDetails = tryPollIp(details.remoteIp); + polledDetails = tryPollIp(details, details.remoteIp); } if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { // Failed, so let's try the fallback if (!localFirst) { - polledDetails = tryPollIp(details.localIp); + polledDetails = tryPollIp(details, details.localIp); } else { - polledDetails = tryPollIp(details.remoteIp); + polledDetails = tryPollIp(details, details.remoteIp); } // The fallback poll worked @@ -414,8 +433,8 @@ public class ComputerManagerService extends Service { } for (ComputerDetails computer : dbManager.getAllComputers()) { - // Add this computer without a thread - pollingTuples.add(new PollingTuple(computer, null)); + // Add tuples for each computer + addTuple(computer); } releaseLocalDatabaseReference(); diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index fbcfa423..787324ef 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -13,6 +13,7 @@ public class PreferenceConfiguration { private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings"; private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio"; private static final String DEADZONE_PREF_STRING = "seekbar_deadzone"; + private static final String LANGUAGE_PREF_STRING = "list_languages"; private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_60 = 10; @@ -27,6 +28,7 @@ public class PreferenceConfiguration { private static final boolean DEFAULT_DISABLE_TOASTS = false; private static final boolean DEFAULT_HOST_AUDIO = false; private static final int DEFAULT_DEADZONE = 15; + private static final String DEFAULT_LANGUAGE = "default"; public static final int FORCE_HARDWARE_DECODER = -1; public static final int AUTOSELECT_DECODER = 0; @@ -37,6 +39,7 @@ public class PreferenceConfiguration { public int decoder; public int deadzonePercentage; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; + public String language; public static int getDefaultBitrate(String resFpsString) { if (resFpsString.equals("720p30")) { @@ -135,6 +138,8 @@ public class PreferenceConfiguration { config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE); + config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE); + // Checkbox preferences config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS); config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS); diff --git a/app/src/main/jni/nv_avc_dec/nv_avc_dec.c b/app/src/main/jni/nv_avc_dec/nv_avc_dec.c index 1209cbc4..500319bd 100644 --- a/app/src/main/jni/nv_avc_dec/nv_avc_dec.c +++ b/app/src/main/jni/nv_avc_dec/nv_avc_dec.c @@ -69,9 +69,6 @@ int nv_avc_init(int width, int height, int perf_lvl, int thread_count) { return -1; } - // Show frames even before a reference frame - decoder_ctx->flags2 |= CODEC_FLAG2_SHOW_ALL; - if (perf_lvl & DISABLE_LOOP_FILTER) { // Skip the loop filter for performance reasons decoder_ctx->skip_loop_filter = AVDISCARD_ALL; @@ -370,17 +367,20 @@ int nv_avc_decode(unsigned char* indata, int inlen) { // Only copy the picture at the end of decoding the packet if (got_pic) { + // Clone the current decode frame outside of the mutex + AVFrame* new_frame = av_frame_clone(dec_frame); + AVFrame* old_frame; + + // Swap it in under lock pthread_mutex_lock(&mutex); - - // Only clone this frame if the last frame was taken. - // This saves on extra copies for frames that don't get - // rendered. - if (yuv_frame == NULL) { - // Clone a new frame - yuv_frame = av_frame_clone(dec_frame); - } - + old_frame = yuv_frame; + yuv_frame = new_frame; pthread_mutex_unlock(&mutex); + + // Free the old frame outside of the mutex + if (old_frame != NULL) { + av_frame_free(&old_frame); + } } return err < 0 ? err : 0; diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 70cea328..7c39b917 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -87,7 +87,7 @@ Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine Mbps Forza video in full-screen - Disabilita i messaggi di warning + Disabilita messaggi di warning Disabilita i messaggi di warning sullo schermo durante lo streaming Lingua Lingua da usare in Limelight @@ -104,5 +104,5 @@ Impostazioni Avanzate Cambia decoder - Il decoder software può aumentare la latenza video quando si usano impostazioni streaming basse + Il decoder software può ridurre la latenza video quando si usano impostazioni streaming basse diff --git a/decoder-errata.txt b/decoder-errata.txt index 0658dd32..577562b4 100644 --- a/decoder-errata.txt +++ b/decoder-errata.txt @@ -4,12 +4,12 @@ This file serves to document some of the decoder errata when using MediaCodec ha - Affected decoders: TI OMAP4, Allwinner A20 2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue. - - Affected decoders: NVIDIA Tegra 3 and 4 + - Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV 3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't - Affected decoders: TI OMAP4 -4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS or first I-frame +4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame - Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only 5. Some decoders will hang if max_dec_frame_buffering is not present