Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e50b7076a1 | |||
| 36ab5aa1b6 | |||
| a0a2b299d9 | |||
| 14d354fc29 | |||
| 342515f916 | |||
| 5f5944c237 | |||
| c025432ad6 | |||
| 171a6437fe | |||
| 11b3648fac | |||
| d1fae89d6d | |||
| 5c06848fe9 | |||
| b50e506e58 | |||
| 59fafa163d | |||
| 22d84b5763 | |||
| 6d186892a8 | |||
| 88d6143897 | |||
| b729fba75e | |||
| c0d3f9fa48 | |||
| af5e7a0e33 | |||
| 371d96ea65 | |||
| e9e332ff85 |
@@ -0,0 +1,4 @@
|
|||||||
|
issuesOpened: >
|
||||||
|
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://discord.gg/MySTSdq) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||||
|
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||||
|
Thank you, and happy streaming!
|
||||||
+2
-2
@@ -7,8 +7,8 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
|
|
||||||
versionName "8.5"
|
versionName "8.7"
|
||||||
versionCode = 204
|
versionCode = 206
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "root"
|
flavorDimensions "root"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.limelight.preferences.PreferenceConfiguration;
|
|||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
import com.limelight.ui.StreamView;
|
import com.limelight.ui.StreamView;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.NetHelper;
|
||||||
import com.limelight.utils.ShortcutHelper;
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
@@ -428,6 +429,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean vpnActive = NetHelper.isActiveNetworkVpn(this);
|
||||||
|
if (vpnActive) {
|
||||||
|
LimeLog.info("Detected active network is a VPN");
|
||||||
|
}
|
||||||
|
|
||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setRefreshRate(prefConfig.fps)
|
||||||
@@ -435,8 +441,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
.setBitrate(prefConfig.bitrate)
|
.setBitrate(prefConfig.bitrate)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
.setMaxPacketSize(1392)
|
.setMaxPacketSize(vpnActive ? 1024 : 1392) // Lower MTU on VPN
|
||||||
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO)
|
.setRemoteConfiguration(vpnActive ? // Use remote optimizations on VPN
|
||||||
|
StreamConfiguration.STREAM_CFG_REMOTE :
|
||||||
|
StreamConfiguration.STREAM_CFG_AUTO)
|
||||||
.setHevcBitratePercentageMultiplier(75)
|
.setHevcBitratePercentageMultiplier(75)
|
||||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||||
.setEnableHdr(willStreamHdr)
|
.setEnableHdr(willStreamHdr)
|
||||||
@@ -587,7 +595,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private float prepareDisplayForRendering() {
|
private float prepareDisplayForRendering() {
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||||
float displayRefreshRate;
|
|
||||||
|
|
||||||
// On M, we can explicitly set the optimal display mode
|
// On M, we can explicitly set the optimal display mode
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@@ -631,7 +638,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||||
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||||
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||||
displayRefreshRate = bestMode.getRefreshRate();
|
|
||||||
}
|
}
|
||||||
// On L, we can at least tell the OS that we want a refresh rate
|
// On L, we can at least tell the OS that we want a refresh rate
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
@@ -652,12 +658,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
||||||
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
||||||
displayRefreshRate = bestRefreshRate;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Otherwise, the active display refresh rate is just
|
// Otherwise, the active display refresh rate is just
|
||||||
// whatever is currently in use.
|
// whatever is currently in use.
|
||||||
displayRefreshRate = display.getRefreshRate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the display mode change
|
// Apply the display mode change
|
||||||
@@ -694,7 +698,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
return displayRefreshRate;
|
// Use the actual refresh rate of the display, since the preferred refresh rate or mode
|
||||||
|
// may not actually be applied (ex: Pixel 4 with Smooth Display disabled).
|
||||||
|
return getWindowManager().getDefaultDisplay().getRefreshRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
@@ -1266,6 +1272,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
for (TouchContext aTouchContext : touchContextMap) {
|
||||||
|
aTouchContext.cancelTouch();
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
|
|
||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
private AudioTrack createAudioTrack(int channelConfig, int sampleRate, int bufferSize, boolean lowLatency) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
48000,
|
sampleRate,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
bufferSize,
|
bufferSize,
|
||||||
@@ -28,7 +28,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
.setUsage(AudioAttributes.USAGE_GAME);
|
.setUsage(AudioAttributes.USAGE_GAME);
|
||||||
AudioFormat format = new AudioFormat.Builder()
|
AudioFormat format = new AudioFormat.Builder()
|
||||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||||
.setSampleRate(48000)
|
.setSampleRate(sampleRate)
|
||||||
.setChannelMask(channelConfig)
|
.setChannelMask(channelConfig)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int setup(int audioConfiguration) {
|
public int setup(int audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bytesPerFrame;
|
int bytesPerFrame;
|
||||||
|
|
||||||
@@ -72,11 +72,11 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
{
|
{
|
||||||
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
|
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
bytesPerFrame = 2 * 240 * 2;
|
bytesPerFrame = 2 * samplesPerFrame * 2;
|
||||||
break;
|
break;
|
||||||
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
|
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
bytesPerFrame = 6 * 240 * 2;
|
bytesPerFrame = 6 * samplesPerFrame * 2;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LimeLog.severe("Decoder returned unhandled channel count");
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
@@ -122,7 +122,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
case 1:
|
case 1:
|
||||||
case 3:
|
case 3:
|
||||||
// Try the larger buffer size
|
// Try the larger buffer size
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
bytesPerFrame * 2);
|
bytesPerFrame * 2);
|
||||||
@@ -135,13 +135,13 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip low latency options if hardware sample rate isn't 48000Hz
|
// Skip low latency options if hardware sample rate doesn't match the content
|
||||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != sampleRate && lowLatency) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
track = createAudioTrack(channelConfig, sampleRate, bufferSize, lowLatency);
|
||||||
track.play();
|
track.play();
|
||||||
|
|
||||||
// Successfully created working AudioTrack. We're done here.
|
// Successfully created working AudioTrack. We're done here.
|
||||||
@@ -170,14 +170,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void playDecodedAudio(short[] audioData) {
|
public void playDecodedAudio(short[] audioData) {
|
||||||
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
||||||
if (MoonBridge.getPendingAudioFrames() < 8) {
|
if (MoonBridge.getPendingAudioDuration() < 40) {
|
||||||
// This will block until the write is completed. That can cause a backlog
|
// This will block until the write is completed. That can cause a backlog
|
||||||
// of pending audio data, so we do the above check to be able to bound
|
// of pending audio data, so we do the above check to be able to bound
|
||||||
// latency at 40 ms in that situation.
|
// latency at 40 ms in that situation.
|
||||||
track.write(audioData, 0, audioData.length);
|
track.write(audioData, 0, audioData.length);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LimeLog.info("Too many pending audio frames: " + MoonBridge.getPendingAudioFrames());
|
LimeLog.info("Too much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -293,12 +293,12 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
movement_radius = getMovementRadius(relative_x, relative_y);
|
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||||
movement_angle = getAngle(relative_x, relative_y);
|
movement_angle = getAngle(relative_x, relative_y);
|
||||||
|
|
||||||
// chop radius if out of outer circle and already pressed
|
// pass touch event to parent if out of outer circle
|
||||||
|
if (movement_radius > radius_complete && !isPressed())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// chop radius if out of outer circle or near the edge
|
||||||
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||||
// not pressed already, so ignore event from outer circle
|
|
||||||
if (!isPressed()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
movement_radius = radius_complete - radius_analog_stick;
|
movement_radius = radius_complete - radius_analog_stick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -683,17 +683,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// for known resolution combinations. Reference frame invalidation may need
|
// for known resolution combinations. Reference frame invalidation may need
|
||||||
// these, so leave them be for those decoders.
|
// these, so leave them be for those decoders.
|
||||||
if (!refFrameInvalidationActive) {
|
if (!refFrameInvalidationActive) {
|
||||||
if (initialWidth <= 720 && initialHeight <= 480) {
|
if (initialWidth <= 720 && initialHeight <= 480 && refreshRate <= 60) {
|
||||||
// Max 5 buffered frames at 720x480x60
|
// Max 5 buffered frames at 720x480x60
|
||||||
LimeLog.info("Patching level_idc to 31");
|
LimeLog.info("Patching level_idc to 31");
|
||||||
sps.levelIdc = 31;
|
sps.levelIdc = 31;
|
||||||
}
|
}
|
||||||
else if (initialWidth <= 1280 && initialHeight <= 720) {
|
else if (initialWidth <= 1280 && initialHeight <= 720 && refreshRate <= 60) {
|
||||||
// Max 5 buffered frames at 1280x720x60
|
// Max 5 buffered frames at 1280x720x60
|
||||||
LimeLog.info("Patching level_idc to 32");
|
LimeLog.info("Patching level_idc to 32");
|
||||||
sps.levelIdc = 32;
|
sps.levelIdc = 32;
|
||||||
}
|
}
|
||||||
else if (initialWidth <= 1920 && initialHeight <= 1080) {
|
else if (initialWidth <= 1920 && initialHeight <= 1080 && refreshRate <= 60) {
|
||||||
// Max 4 buffered frames at 1920x1080x64
|
// Max 4 buffered frames at 1920x1080x64
|
||||||
LimeLog.info("Patching level_idc to 42");
|
LimeLog.info("Patching level_idc to 42");
|
||||||
sps.levelIdc = 42;
|
sps.levelIdc = 42;
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ public class MediaCodecHelper {
|
|||||||
// These decoders have low enough input buffer latency that they
|
// These decoders have low enough input buffer latency that they
|
||||||
// can be directly invoked from the receive thread
|
// can be directly invoked from the receive thread
|
||||||
directSubmitPrefixes.add("omx.qcom");
|
directSubmitPrefixes.add("omx.qcom");
|
||||||
directSubmitPrefixes.add("c2.qti");
|
|
||||||
directSubmitPrefixes.add("omx.sec");
|
directSubmitPrefixes.add("omx.sec");
|
||||||
directSubmitPrefixes.add("omx.exynos");
|
directSubmitPrefixes.add("omx.exynos");
|
||||||
directSubmitPrefixes.add("omx.intel");
|
directSubmitPrefixes.add("omx.intel");
|
||||||
@@ -56,6 +55,9 @@ public class MediaCodecHelper {
|
|||||||
directSubmitPrefixes.add("omx.TI");
|
directSubmitPrefixes.add("omx.TI");
|
||||||
directSubmitPrefixes.add("omx.arc");
|
directSubmitPrefixes.add("omx.arc");
|
||||||
directSubmitPrefixes.add("omx.nvidia");
|
directSubmitPrefixes.add("omx.nvidia");
|
||||||
|
|
||||||
|
// All Codec2 decoders
|
||||||
|
directSubmitPrefixes.add("c2.");
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@@ -149,6 +151,9 @@ public class MediaCodecHelper {
|
|||||||
//whitelistedHevcDecoders.add("omx.amlogic");
|
//whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
//whitelistedHevcDecoders.add("omx.rk");
|
//whitelistedHevcDecoders.add("omx.rk");
|
||||||
|
|
||||||
|
// Let's see if HEVC decoders are finally stable with C2
|
||||||
|
whitelistedHevcDecoders.add("c2.");
|
||||||
|
|
||||||
// Based on GPU attributes queried at runtime, the omx.qcom/c2.qti prefix will be added
|
// Based on GPU attributes queried at runtime, the omx.qcom/c2.qti prefix will be added
|
||||||
// during initialization to avoid SoCs with broken HEVC decoders.
|
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||||
}
|
}
|
||||||
@@ -277,7 +282,6 @@ public class MediaCodecHelper {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
||||||
blacklistedDecoderPrefixes.add("c2.qti.hevc.decoder");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
|
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
|
||||||
@@ -424,9 +428,14 @@ public class MediaCodecHelper {
|
|||||||
// typically because it can't support reference frame invalidation.
|
// typically because it can't support reference frame invalidation.
|
||||||
// However, we will use it for HDR and for streaming over mobile networks
|
// However, we will use it for HDR and for streaming over mobile networks
|
||||||
// since it works fine otherwise.
|
// since it works fine otherwise.
|
||||||
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
if (isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
||||||
LimeLog.info("Selected deprioritized decoder");
|
if (meteredData) {
|
||||||
return true;
|
LimeLog.info("Selected deprioritized decoder");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import java.util.HashSet;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
@@ -22,13 +24,19 @@ import com.limelight.nvstream.http.PairingManager;
|
|||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
|
import com.limelight.utils.NetHelper;
|
||||||
import com.limelight.utils.ServerHelper;
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.net.NetworkCapabilities;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
@@ -54,6 +62,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private ComputerManagerListener listener = null;
|
private ComputerManagerListener listener = null;
|
||||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
private boolean pollingActive = false;
|
private boolean pollingActive = false;
|
||||||
|
private final Lock defaultNetworkLock = new ReentrantLock();
|
||||||
|
|
||||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||||
@@ -294,6 +303,66 @@ public class ComputerManagerService extends Service {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void populateExternalAddress(ComputerDetails details) {
|
||||||
|
boolean boundToNetwork = false;
|
||||||
|
boolean activeNetworkIsVpn = NetHelper.isActiveNetworkVpn(this);
|
||||||
|
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
|
||||||
|
// Check if we're currently connected to a VPN which may send our
|
||||||
|
// STUN request from an unexpected interface
|
||||||
|
if (activeNetworkIsVpn) {
|
||||||
|
// Acquire the default network lock since we could be changing global process state
|
||||||
|
defaultNetworkLock.lock();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// On Lollipop or later, we can bind our process to the underlying interface
|
||||||
|
// to ensure our STUN request goes out on that interface or not at all (which is
|
||||||
|
// preferable to getting a VPN endpoint address back).
|
||||||
|
Network[] networks = connMgr.getAllNetworks();
|
||||||
|
for (Network net : networks) {
|
||||||
|
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(net);
|
||||||
|
if (netCaps != null) {
|
||||||
|
if (!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
||||||
|
!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||||
|
// This network looks like an underlying multicast-capable transport,
|
||||||
|
// so let's guess that it's probably where our mDNS response came from.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (connMgr.bindProcessToNetwork(net)) {
|
||||||
|
boundToNetwork = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ConnectivityManager.setProcessDefaultNetwork(net)) {
|
||||||
|
boundToNetwork = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the STUN request if we're not on a VPN or if we bound to a network
|
||||||
|
if (!activeNetworkIsVpn || boundToNetwork) {
|
||||||
|
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unbind from the network
|
||||||
|
if (boundToNetwork) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
connMgr.bindProcessToNetwork(null);
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
ConnectivityManager.setProcessDefaultNetwork(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock the network state
|
||||||
|
if (activeNetworkIsVpn) {
|
||||||
|
defaultNetworkLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MdnsDiscoveryListener createDiscoveryListener() {
|
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||||
return new MdnsDiscoveryListener() {
|
return new MdnsDiscoveryListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -308,7 +377,7 @@ public class ComputerManagerService extends Service {
|
|||||||
// our WAN address, which is also very likely the WAN address
|
// our WAN address, which is also very likely the WAN address
|
||||||
// of the PC. We can use this later to connect remotely.
|
// of the PC. We can use this later to connect remotely.
|
||||||
if (computer.getLocalAddress() instanceof Inet4Address) {
|
if (computer.getLocalAddress() instanceof Inet4Address) {
|
||||||
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
populateExternalAddress(details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (computer.getIpv6Address() != null) {
|
if (computer.getIpv6Address() != null) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -76,7 +77,8 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||||
new NetworkAssetLoader(context, uniqueId),
|
new NetworkAssetLoader(context, uniqueId),
|
||||||
new MemoryAssetLoader(),
|
new MemoryAssetLoader(),
|
||||||
new DiskAssetLoader(context));
|
new DiskAssetLoader(context),
|
||||||
|
BitmapFactory.decodeResource(context.getResources(), R.drawable.no_app_image));
|
||||||
|
|
||||||
// This will trigger the view to reload with the new layout
|
// This will trigger the view to reload with the new layout
|
||||||
setLayoutId(getLayoutIdForPreferences(prefs));
|
setLayoutId(getLayoutIdForPreferences(prefs));
|
||||||
|
|||||||
@@ -52,15 +52,17 @@ public class CachedAppAssetLoader {
|
|||||||
private final MemoryAssetLoader memoryLoader;
|
private final MemoryAssetLoader memoryLoader;
|
||||||
private final DiskAssetLoader diskLoader;
|
private final DiskAssetLoader diskLoader;
|
||||||
private final Bitmap placeholderBitmap;
|
private final Bitmap placeholderBitmap;
|
||||||
|
private final Bitmap noAppImageBitmap;
|
||||||
|
|
||||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||||
DiskAssetLoader diskLoader) {
|
DiskAssetLoader diskLoader, Bitmap noAppImageBitmap) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.scalingDivider = scalingDivider;
|
this.scalingDivider = scalingDivider;
|
||||||
this.networkLoader = networkLoader;
|
this.networkLoader = networkLoader;
|
||||||
this.memoryLoader = memoryLoader;
|
this.memoryLoader = memoryLoader;
|
||||||
this.diskLoader = diskLoader;
|
this.diskLoader = diskLoader;
|
||||||
|
this.noAppImageBitmap = noAppImageBitmap;
|
||||||
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,9 +190,10 @@ public class CachedAppAssetLoader {
|
|||||||
prgView.setVisibility(View.VISIBLE);
|
prgView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set off another loader task on the network executor
|
// Set off another loader task on the network executor. This time our AsyncDrawable
|
||||||
|
// will use the app image placeholder bitmap, rather than an empty bitmap.
|
||||||
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
||||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
|
||||||
imageView.setVisibility(View.VISIBLE);
|
imageView.setVisibility(View.VISIBLE);
|
||||||
imageView.setImageDrawable(asyncDrawable);
|
imageView.setImageDrawable(asyncDrawable);
|
||||||
task.executeOnExecutor(networkExecutor, tuple);
|
task.executeOnExecutor(networkExecutor, tuple);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.limelight.nvstream.av.audio;
|
package com.limelight.nvstream.av.audio;
|
||||||
|
|
||||||
public interface AudioRenderer {
|
public interface AudioRenderer {
|
||||||
int setup(int audioConfiguration);
|
int setup(int audioConfiguration, int sampleRate, int samplesPerFrame);
|
||||||
|
|
||||||
void start();
|
void start();
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ public class PairingManager {
|
|||||||
if (serverCert == null) {
|
if (serverCert == null) {
|
||||||
// Attempting to pair while another device is pairing will cause GFE
|
// Attempting to pair while another device is pairing will cause GFE
|
||||||
// to give an empty cert in the response.
|
// to give an empty cert in the response.
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
return PairState.ALREADY_IN_PROGRESS;
|
return PairState.ALREADY_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ public class MoonBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int bridgeArInit(int audioConfiguration) {
|
public static int bridgeArInit(int audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||||
if (audioRenderer != null) {
|
if (audioRenderer != null) {
|
||||||
return audioRenderer.setup(audioConfiguration);
|
return audioRenderer.setup(audioConfiguration, sampleRate, samplesPerFrame);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -208,7 +208,7 @@ public class MoonBridge {
|
|||||||
|
|
||||||
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
||||||
|
|
||||||
public static native int getPendingAudioFrames();
|
public static native int getPendingAudioDuration();
|
||||||
|
|
||||||
public static native int getPendingVideoFrames();
|
public static native int getPendingVideoFrames();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.net.NetworkCapabilities;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
public class NetHelper {
|
||||||
|
public static boolean isActiveNetworkVpn(Context context) {
|
||||||
|
ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Network activeNetwork = connMgr.getActiveNetwork();
|
||||||
|
if (activeNetwork != null) {
|
||||||
|
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
|
||||||
|
if (netCaps != null) {
|
||||||
|
return netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||||
|
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||||
|
if (activeNetworkInfo != null) {
|
||||||
|
return activeNetworkInfo.getType() == ConnectivityManager.TYPE_VPN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,7 +80,7 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
|
|||||||
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
|
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
|
||||||
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
|
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
|
||||||
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I");
|
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I");
|
||||||
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(I)I");
|
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(III)I");
|
||||||
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
|
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
|
||||||
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
|
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
|
||||||
BridgeArCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArCleanup", "()V");
|
BridgeArCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArCleanup", "()V");
|
||||||
@@ -206,7 +206,7 @@ int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusCon
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration);
|
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration, opusConfig->sampleRate, opusConfig->samplesPerFrame);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
err = -1;
|
err = -1;
|
||||||
}
|
}
|
||||||
@@ -382,6 +382,7 @@ static AUDIO_RENDERER_CALLBACKS BridgeAudioRendererCallbacks = {
|
|||||||
.stop = BridgeArStop,
|
.stop = BridgeArStop,
|
||||||
.cleanup = BridgeArCleanup,
|
.cleanup = BridgeArCleanup,
|
||||||
.decodeAndPlaySample = BridgeArDecodeAndPlaySample,
|
.decodeAndPlaySample = BridgeArDecodeAndPlaySample,
|
||||||
|
.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION
|
||||||
};
|
};
|
||||||
|
|
||||||
static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
|
static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
|
||||||
|
|||||||
Submodule app/src/main/jni/moonlight-core/moonlight-common-c updated: 9bd301897a...f5ae5df5d0
@@ -83,8 +83,8 @@ Java_com_limelight_nvstream_jni_MoonBridge_findExternalAddressIP4(JNIEnv *env, j
|
|||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_com_limelight_nvstream_jni_MoonBridge_getPendingAudioFrames(JNIEnv *env, jclass clazz) {
|
Java_com_limelight_nvstream_jni_MoonBridge_getPendingAudioDuration(JNIEnv *env, jclass clazz) {
|
||||||
return LiGetPendingAudioFrames();
|
return LiGetPendingAudioDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -8,14 +8,6 @@
|
|||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/grid_spinner"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_width="75dp"
|
|
||||||
android:layout_height="75dp"
|
|
||||||
android:indeterminate="true">
|
|
||||||
</ProgressBar>
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/grid_image"
|
android:id="@+id/grid_image"
|
||||||
android:cropToPadding="false"
|
android:cropToPadding="false"
|
||||||
@@ -24,6 +16,14 @@
|
|||||||
android:layout_width="150dp"
|
android:layout_width="150dp"
|
||||||
android:layout_height="175dp">
|
android:layout_height="175dp">
|
||||||
</ImageView>
|
</ImageView>
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/grid_spinner"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_width="75dp"
|
||||||
|
android:layout_height="75dp"
|
||||||
|
android:indeterminate="true">
|
||||||
|
</ProgressBar>
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/grid_overlay"
|
android:id="@+id/grid_overlay"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
|
|||||||
@@ -8,14 +8,6 @@
|
|||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/grid_spinner"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_width="50dp"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:indeterminate="true">
|
|
||||||
</ProgressBar>
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/grid_image"
|
android:id="@+id/grid_image"
|
||||||
android:cropToPadding="false"
|
android:cropToPadding="false"
|
||||||
@@ -24,6 +16,14 @@
|
|||||||
android:layout_width="100dp"
|
android:layout_width="100dp"
|
||||||
android:layout_height="117dp">
|
android:layout_height="117dp">
|
||||||
</ImageView>
|
</ImageView>
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/grid_spinner"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:indeterminate="true">
|
||||||
|
</ProgressBar>
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/grid_overlay"
|
android:id="@+id/grid_overlay"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
classpath 'com.android.tools.build:gradle:3.5.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Fixed false touch events when using the back gesture on Android 10
|
||||||
|
- Fixed external IP address detection with certain VPN apps
|
||||||
|
- Display a placeholder image when box art is loading
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Fixed RTSP handshake error when streaming from certain networks
|
||||||
|
- Improved performance when streaming over a VPN
|
||||||
|
- Reduced audio bandwidth usage when streaming over low speed connections
|
||||||
|
- Fixed hitbox of on-screen analog sticks
|
||||||
Reference in New Issue
Block a user