From cdf2ff7d93b45338b1cdc243d1c34c7915e76b68 Mon Sep 17 00:00:00 2001 From: Matt Bargar Date: Fri, 6 Nov 2026 22:31:29 -0500 Subject: [PATCH] Forward native GameStream microphone audio on Android Add per-run consent and recording permission, AudioRecord capture with Opus/JNI transport, an active indicator, and joined shutdown on activity pause and connection teardown. --- README.md | 18 +++ app/src/main/AndroidManifest.xml | 2 + app/src/main/java/com/limelight/Game.java | 11 ++ .../com/limelight/nvstream/NvConnection.java | 32 +++- .../nvstream/NvConnectionListener.java | 1 + .../nvstream/StreamConfiguration.java | 10 ++ .../limelight/nvstream/jni/MoonBridge.java | 2 +- .../nvstream/jni/NativeMicCapture.java | 144 ++++++++++++++++++ .../preferences/PreferenceConfiguration.java | 11 ++ .../limelight/preferences/StreamSettings.java | 30 ++++ app/src/main/jni/moonlight-core/Android.mk | 5 +- .../jni/moonlight-core/NativeMicEncoder.c | 33 ++++ .../jni/moonlight-core/NativeMicEncoder.h | 20 +++ .../jni/moonlight-core/android-mic-bridge.c | 67 ++++++++ app/src/main/jni/moonlight-core/callbacks.c | 5 +- .../jni/moonlight-core/moonlight-common-c | 2 +- app/src/main/res/layout/activity_game.xml | 11 ++ app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/preferences.xml | 6 + 19 files changed, 408 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/limelight/nvstream/jni/NativeMicCapture.java create mode 100644 app/src/main/jni/moonlight-core/NativeMicEncoder.c create mode 100644 app/src/main/jni/moonlight-core/NativeMicEncoder.h create mode 100644 app/src/main/jni/moonlight-core/android-mic-bridge.c diff --git a/README.md b/README.md index 0b1bbe780e..3bab0bcd4d 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,21 @@ You can follow development on our [Discord server](https://moonlight-stream.org/ Moonlight is the work of students at [Case Western](http://case.edu) and was started as a project at [MHacks](http://mhacks.org). + +## Native GameStream microphone + +On Android 6 or later, enable **Forward microphone to NVIDIA GameStream +for this run** in streaming settings and grant microphone permission. +This setting starts off for each app process. Capture starts after successful +native GameStream negotiation, shows a stream indicator, and stops when the +activity pauses or the connection ends. Reconnect to enable it again after +leaving the foreground. + +Use a compatible [Windows NVIDIA GameStream host](https://github.com/Bargs/native-gamestream-microphone-host) +on a trusted private IPv4 network or separately secured tunnel. The legacy +CBC mode does not authenticate microphone packets. This path is distinct +from Sunshine microphone extensions. + +The arm64 non-root debug app builds with SDK 37 and NDK 29. Physical +microphone, permission, route changes, pause and native host delivery +still need device verification. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d916899e6b..8c26933276 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 8d369b3eab..b2cfc4fe10 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -478,6 +478,7 @@ public void notifyCrash(Exception e) { .setAudioConfiguration(prefConfig.audioConfiguration) .setColorSpace(decoderRenderer.getPreferredColorSpace()) .setColorRange(decoderRenderer.getPreferredColorRange()) + .setNativeMicrophone(prefConfig.nativeMicrophone) .setPersistGamepadsAfterDisconnect(!prefConfig.multiController) .build(); @@ -1062,6 +1063,10 @@ protected void onDestroy() { @Override protected void onPause() { + if (conn != null) { + conn.stopMicrophone(); + } + if (isFinishing()) { // Stop any further input device notifications before we lose focus (and pointer capture) if (controllerHandler != null) { @@ -2460,6 +2465,12 @@ public void run() { }); } + @Override + public void microphoneStateChanged(boolean active) { + runOnUiThread(() -> findViewById(R.id.microphoneIndicator) + .setVisibility(active ? View.VISIBLE : View.GONE)); + } + @Override public void displayTransientMessage(final String message) { if (!prefConfig.disableWarnings) { diff --git a/app/src/main/java/com/limelight/nvstream/NvConnection.java b/app/src/main/java/com/limelight/nvstream/NvConnection.java index f29563f1c1..9d1c81e891 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnection.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnection.java @@ -39,6 +39,8 @@ import com.limelight.nvstream.http.PairingManager; import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.jni.MoonBridge; +import com.limelight.nvstream.jni.NativeMicCapture; +import com.limelight.R; public class NvConnection { // Context parameters @@ -48,6 +50,19 @@ public class NvConnection { private static Semaphore connectionAllowed = new Semaphore(1); private final boolean isMonkey; private final Context appContext; + private final Object microphoneLock = new Object(); + private boolean microphoneAllowed = true; + private NativeMicCapture microphone; + + public void stopMicrophone() { + synchronized (microphoneLock) { + microphoneAllowed = false; + if (microphone != null) { + microphone.close(); + microphone = null; + } + } + } public NvConnection(Context appContext, ComputerDetails.AddressTuple host, int httpsPort, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert) { @@ -87,6 +102,7 @@ private static int generateRiKeyId() { } public void stop() { + stopMicrophone(); // Interrupt any pending connection. This is thread-safe. MoonBridge.interruptConnection(); @@ -425,6 +441,11 @@ public void run() { // we must not invoke that functionality in parallel. synchronized (MoonBridge.class) { MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener); + boolean requestMicrophone; + synchronized (microphoneLock) { + requestMicrophone = microphoneAllowed && context.streamConfig.getNativeMicrophone() && + NativeMicCapture.hasPermission(appContext); + } int ret = MoonBridge.startConnection(context.serverAddress.address, context.serverAppVersion, context.serverGfeVersion, context.rtspSessionUrl, context.serverCodecModeSupport, @@ -437,7 +458,7 @@ public void run() { context.riKey.getEncoded(), ib.array(), context.videoCapabilities, context.streamConfig.getColorSpace(), - context.streamConfig.getColorRange()); + context.streamConfig.getColorRange(), requestMicrophone); if (ret != 0) { // LiStartConnection() failed, so the caller is not expected // to stop the connection themselves. We need to release their @@ -445,6 +466,15 @@ public void run() { connectionAllowed.release(); return; } + synchronized (microphoneLock) { + if (microphoneAllowed && requestMicrophone) { + microphone = NativeMicCapture.start(appContext, connectionListener::microphoneStateChanged); + if (microphone == null) { + connectionListener.displayTransientMessage( + appContext.getString(R.string.native_microphone_unavailable)); + } + } + } } } }).start(); diff --git a/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java b/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java index a6109dbfe5..4e9dca46fd 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java @@ -8,6 +8,7 @@ public interface NvConnectionListener { void connectionStarted(); void connectionTerminated(int errorCode); void connectionStatusUpdate(int connectionStatus); + default void microphoneStateChanged(boolean active) {} void displayMessage(String message); void displayTransientMessage(String message); diff --git a/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java b/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java index 317f9381c1..2abc319263 100644 --- a/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java +++ b/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java @@ -26,6 +26,11 @@ public class StreamConfiguration { private int attachedGamepadMask; private int encryptionFlags; private int colorRange; + private boolean nativeMicrophone; + + public boolean getNativeMicrophone() { + return nativeMicrophone; + } private int colorSpace; private boolean persistGamepadsAfterDisconnect; @@ -118,6 +123,11 @@ public StreamConfiguration.Builder setSupportedVideoFormats(int supportedVideoFo return this; } + public StreamConfiguration.Builder setNativeMicrophone(boolean enabled) { + config.nativeMicrophone = enabled; + return this; + } + public StreamConfiguration.Builder setColorRange(int colorRange) { config.colorRange = colorRange; return this; diff --git a/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java b/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java index 0dcafff559..fdb448620f 100644 --- a/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java +++ b/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java @@ -348,7 +348,7 @@ public static native int startConnection(String address, String appVersion, Stri int clientRefreshRateX100, byte[] riAesKey, byte[] riAesIv, int videoCapabilities, - int colorSpace, int colorRange); + int colorSpace, int colorRange, boolean requestNativeMicrophone); public static native void stopConnection(); diff --git a/app/src/main/java/com/limelight/nvstream/jni/NativeMicCapture.java b/app/src/main/java/com/limelight/nvstream/jni/NativeMicCapture.java new file mode 100644 index 0000000000..9f9a08ec35 --- /dev/null +++ b/app/src/main/java/com/limelight/nvstream/jni/NativeMicCapture.java @@ -0,0 +1,144 @@ +package com.limelight.nvstream.jni; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.os.Build; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** Owned by a single opted-in connection. Close before native connection teardown. */ +public final class NativeMicCapture implements AutoCloseable { + private static final int SAMPLE_RATE = 48000; + private static final int FRAME_SAMPLES = 240; + + static { + System.loadLibrary("moonlight-core"); + } + + private final AtomicBoolean stopping = new AtomicBoolean(); + private final Thread worker; + public interface Listener { + void onMicrophoneStateChanged(boolean active); + } + + private NativeMicCapture(AudioRecord input, long handle, Listener listener) { + worker = new Thread(() -> capture(input, handle, listener), "Moonlight microphone"); + worker.start(); + } + + public static boolean hasPermission(Context context) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED; + } + + /** Call after successful native connection start, while foreground and opted in. */ + public static NativeMicCapture start(Context context, Listener listener) { + if (!hasPermission(context)) { + return null; + } + long handle = create(); + if (handle == 0) { + return null; + } + + AudioRecord input = null; + boolean handedOff = false; + try { + int minimum = AudioRecord.getMinBufferSize(SAMPLE_RATE, + AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); + if (minimum <= 0) { + return null; + } + input = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, + AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, + Math.max(minimum, FRAME_SAMPLES * Short.BYTES * 4)); + if (input.getState() != AudioRecord.STATE_INITIALIZED) { + return null; + } + NativeMicCapture capture = new NativeMicCapture(input, handle, listener); + handedOff = true; + return capture; + } catch (IllegalArgumentException | SecurityException e) { + return null; + } finally { + if (!handedOff) { + try { + if (input != null) { + input.release(); + } + } finally { + destroy(handle); + } + } + } + } + + private void capture(AudioRecord input, long handle, Listener listener) { + try { + short[] frame = new short[FRAME_SAMPLES]; + int filled = 0; + if (stopping.get()) { + return; + } + input.startRecording(); + if (input.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { + return; + } + listener.onMicrophoneStateChanged(true); + while (!stopping.get()) { + int read = input.read(frame, filled, FRAME_SAMPLES - filled, + AudioRecord.READ_NON_BLOCKING); + if (read < 0) { + return; + } + if (read == 0) { + Thread.sleep(2); + continue; + } + filled += read; + if (filled == FRAME_SAMPLES) { + if (stopping.get() || send(handle, frame) != 0) { + return; + } + filled = 0; + } + } + } catch (IllegalStateException | SecurityException | InterruptedException e) { + // Device loss, permission change or owner shutdown ends this capture. + } finally { + try { + input.release(); + } finally { + destroy(handle); + listener.onMicrophoneStateChanged(false); + } + } + } + + @Override + public void close() { + stopping.set(true); + worker.interrupt(); + boolean interrupted = false; + for (;;) { + try { + worker.join(); + break; + } catch (InterruptedException e) { + interrupted = true; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + private static native long create(); + private static native int send(long handle, short[] frame); + private static native void destroy(long handle); +} diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index 8ed01e3610..3b166a5be5 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -127,6 +127,16 @@ public enum AnalogStickForScrolling { public FormatOption videoFormat; public int deadzonePercentage; public int oscOpacity; + private static volatile boolean nativeMicrophoneForRun; + public boolean nativeMicrophone; + + static boolean isNativeMicrophoneEnabledForRun() { + return nativeMicrophoneForRun; + } + + static void setNativeMicrophoneEnabledForRun(boolean enabled) { + nativeMicrophoneForRun = enabled; + } public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public String language; public boolean smallIconMode, multiController, usbDriver, flipFaceButtons; @@ -574,6 +584,7 @@ else if (audioConfig.equals("51")) { config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS); config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS); config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); + config.nativeMicrophone = isNativeMicrophoneEnabledForRun(); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)); config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER); diff --git a/app/src/main/java/com/limelight/preferences/StreamSettings.java b/app/src/main/java/com/limelight/preferences/StreamSettings.java index 7070104168..b1f94ea3e1 100644 --- a/app/src/main/java/com/limelight/preferences/StreamSettings.java +++ b/app/src/main/java/com/limelight/preferences/StreamSettings.java @@ -1,5 +1,6 @@ package com.limelight.preferences; +import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -122,6 +123,20 @@ public void onBackPressed() { } public static class SettingsFragment extends PreferenceFragment { + private static final int NATIVE_MIC_PERMISSION = 211; + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == NATIVE_MIC_PERMISSION && grantResults.length == 1 && + grantResults[0] == PackageManager.PERMISSION_GRANTED && isAdded()) { + CheckBoxPreference microphone = (CheckBoxPreference) findPreference("checkbox_native_microphone"); + if (microphone != null) { + PreferenceConfiguration.setNativeMicrophoneEnabledForRun(true); + microphone.setChecked(true); + } + } + } private int nativeResolutionStartIndex = Integer.MAX_VALUE; private boolean nativeFramerateShown = false; @@ -276,6 +291,21 @@ public void onCreate(Bundle savedInstanceState) { addPreferencesFromResource(R.xml.preferences); PreferenceScreen screen = getPreferenceScreen(); + CheckBoxPreference microphone = (CheckBoxPreference) findPreference("checkbox_native_microphone"); + microphone.setChecked(PreferenceConfiguration.isNativeMicrophoneEnabledForRun()); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + microphone.setEnabled(false); + } else { + microphone.setOnPreferenceChangeListener((preference, value) -> { + if (Boolean.TRUE.equals(value) && + getActivity().checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, NATIVE_MIC_PERMISSION); + return false; + } + PreferenceConfiguration.setNativeMicrophoneEnabledForRun(Boolean.TRUE.equals(value)); + return true; + }); + } // hide on-screen controls category on non touch screen devices if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) { diff --git a/app/src/main/jni/moonlight-core/Android.mk b/app/src/main/jni/moonlight-core/Android.mk index f32fc9dc4b..471f65ab0a 100644 --- a/app/src/main/jni/moonlight-core/Android.mk +++ b/app/src/main/jni/moonlight-core/Android.mk @@ -8,7 +8,10 @@ LOCAL_PATH := $(MY_LOCAL_PATH) include $(CLEAR_VARS) LOCAL_MODULE := moonlight-core -LOCAL_SRC_FILES := moonlight-common-c/src/AudioStream.c \ +LOCAL_SRC_FILES := NativeMicEncoder.c \ + android-mic-bridge.c \ + moonlight-common-c/src/NativeMicStream.c \ + moonlight-common-c/src/AudioStream.c \ moonlight-common-c/src/ByteBuffer.c \ moonlight-common-c/src/Connection.c \ moonlight-common-c/src/ConnectionTester.c \ diff --git a/app/src/main/jni/moonlight-core/NativeMicEncoder.c b/app/src/main/jni/moonlight-core/NativeMicEncoder.c new file mode 100644 index 0000000000..f9257a48f8 --- /dev/null +++ b/app/src/main/jni/moonlight-core/NativeMicEncoder.c @@ -0,0 +1,33 @@ +#include "NativeMicEncoder.h" + +OpusEncoder* NativeMicEncoderCreate(void) +{ + int error; + OpusEncoder* encoder = opus_encoder_create(NATIVE_MIC_SAMPLE_RATE, 1, + OPUS_APPLICATION_RESTRICTED_LOWDELAY, &error); + if (encoder == NULL) { + return NULL; + } + if (opus_encoder_ctl(encoder, OPUS_SET_BITRATE(32000)) != OPUS_OK || + opus_encoder_ctl(encoder, OPUS_SET_VBR(0)) != OPUS_OK || + opus_encoder_ctl(encoder, OPUS_SET_DTX(0)) != OPUS_OK) { + opus_encoder_destroy(encoder); + return NULL; + } + return encoder; +} + +int NativeMicEncode(OpusEncoder* encoder, const int16_t* pcm, size_t samples, + unsigned char* packet, size_t capacity) +{ + if (encoder == NULL || pcm == NULL || packet == NULL || + samples != NATIVE_MIC_FRAME_SAMPLES || capacity < NATIVE_MIC_FRAME_BYTES) { + return OPUS_BAD_ARG; + } + int bytes = opus_encode(encoder, pcm, NATIVE_MIC_FRAME_SAMPLES, packet, + NATIVE_MIC_FRAME_BYTES); + if (bytes < 0) { + return bytes; + } + return bytes == NATIVE_MIC_FRAME_BYTES ? OPUS_OK : OPUS_INTERNAL_ERROR; +} diff --git a/app/src/main/jni/moonlight-core/NativeMicEncoder.h b/app/src/main/jni/moonlight-core/NativeMicEncoder.h new file mode 100644 index 0000000000..99992b364d --- /dev/null +++ b/app/src/main/jni/moonlight-core/NativeMicEncoder.h @@ -0,0 +1,20 @@ +#ifndef NATIVE_MIC_ENCODER_H +#define NATIVE_MIC_ENCODER_H + +#include +#include +#include + +enum { + NATIVE_MIC_SAMPLE_RATE = 48000, + NATIVE_MIC_FRAME_SAMPLES = 240, + NATIVE_MIC_FRAME_BYTES = 20 +}; + +// Serialize encoder use and destruction. Ownership may transfer before encoding. +OpusEncoder* NativeMicEncoderCreate(void); +// Returns zero for a complete packet and a negative Opus error otherwise. +int NativeMicEncode(OpusEncoder* encoder, const int16_t* pcm, size_t samples, + unsigned char* packet, size_t capacity); + +#endif diff --git a/app/src/main/jni/moonlight-core/android-mic-bridge.c b/app/src/main/jni/moonlight-core/android-mic-bridge.c new file mode 100644 index 0000000000..f3aa656b3a --- /dev/null +++ b/app/src/main/jni/moonlight-core/android-mic-bridge.c @@ -0,0 +1,67 @@ +#include +#include +#include + +#include "Limelight.h" +#include "NativeMicEncoder.h" + +typedef struct { + NATIVE_MIC_SENDER* sender; + OpusEncoder* encoder; +} AndroidMic; + +JNIEXPORT jlong JNICALL +Java_com_limelight_nvstream_jni_NativeMicCapture_create(JNIEnv* env, jclass type) +{ + (void)env; + (void)type; + AndroidMic* mic = calloc(1, sizeof(*mic)); + if (!mic) { + return 0; + } + mic->encoder = NativeMicEncoderCreate(); + if (mic->encoder) { + mic->sender = LiCreateNativeMicSender(); + } + if (!mic->sender) { + if (mic->encoder) { + opus_encoder_destroy(mic->encoder); + } + free(mic); + return 0; + } + return (jlong)(intptr_t)mic; +} + +JNIEXPORT jint JNICALL +Java_com_limelight_nvstream_jni_NativeMicCapture_send(JNIEnv* env, jclass type, + jlong handle, jshortArray frame) +{ + (void)type; + AndroidMic* mic = (AndroidMic*)(intptr_t)handle; + if (!mic || !frame || (*env)->GetArrayLength(env, frame) != NATIVE_MIC_FRAME_SAMPLES) { + return -1; + } + jshort pcm[NATIVE_MIC_FRAME_SAMPLES]; + unsigned char packet[NATIVE_MIC_FRAME_BYTES]; + (*env)->GetShortArrayRegion(env, frame, 0, NATIVE_MIC_FRAME_SAMPLES, pcm); + if ((*env)->ExceptionCheck(env) || + NativeMicEncode(mic->encoder, pcm, NATIVE_MIC_FRAME_SAMPLES, + packet, sizeof(packet)) != OPUS_OK) { + return -1; + } + return LiSendNativeMicFrame(mic->sender, packet, sizeof(packet)); +} + +JNIEXPORT void JNICALL +Java_com_limelight_nvstream_jni_NativeMicCapture_destroy(JNIEnv* env, jclass type, jlong handle) +{ + (void)env; + (void)type; + AndroidMic* mic = (AndroidMic*)(intptr_t)handle; + if (mic) { + LiDestroyNativeMicSender(mic->sender); + opus_encoder_destroy(mic->encoder); + free(mic); + } +} diff --git a/app/src/main/jni/moonlight-core/callbacks.c b/app/src/main/jni/moonlight-core/callbacks.c index b9be69177b..c1ae411990 100644 --- a/app/src/main/jni/moonlight-core/callbacks.c +++ b/app/src/main/jni/moonlight-core/callbacks.c @@ -461,7 +461,7 @@ Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass c jint clientRefreshRateX100, jbyteArray riAesKey, jbyteArray riAesIv, jint videoCapabilities, - jint colorSpace, jint colorRange) { + jint colorSpace, jint colorRange, jboolean requestNativeMicrophone) { SERVER_INFORMATION serverInfo = { .address = (*env)->GetStringUTFChars(env, address, 0), .serverInfoAppVersion = (*env)->GetStringUTFChars(env, appVersion, 0), @@ -481,7 +481,8 @@ Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass c .clientRefreshRateX100 = clientRefreshRateX100, .encryptionFlags = ENCFLG_AUDIO, .colorSpace = colorSpace, - .colorRange = colorRange + .colorRange = colorRange, + .requestNativeMic = requestNativeMicrophone }; jbyte* riAesKeyBuf = (*env)->GetByteArrayElements(env, riAesKey, NULL); diff --git a/app/src/main/jni/moonlight-core/moonlight-common-c b/app/src/main/jni/moonlight-core/moonlight-common-c index 874ac9548f..a9107cf1f5 160000 --- a/app/src/main/jni/moonlight-core/moonlight-common-c +++ b/app/src/main/jni/moonlight-core/moonlight-common-c @@ -1 +1 @@ -Subproject commit 874ac9548f1bd6f095ef2b435c42cdde460e7821 +Subproject commit a9107cf1f54e50be328573f9a79de31d2f3fdcad diff --git a/app/src/main/res/layout/activity_game.xml b/app/src/main/res/layout/activity_game.xml index 85a960d963..5b93b70bb3 100644 --- a/app/src/main/res/layout/activity_game.xml +++ b/app/src/main/res/layout/activity_game.xml @@ -50,4 +50,15 @@ android:preferKeepClear="true" android:visibility="gone" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b20e8beecc..d8e2deda5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -303,4 +303,8 @@ None (both sticks move the mouse) Right analog stick Left analog stick + Forward microphone to NVIDIA GameStream for this run + Microphone forwarding ON + Microphone forwarding could not start + Requires Android 6 or later and a compatible NVIDIA host on private IPv4. Pausing or ending the stream stops forwarding. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 5b09082b35..01985f533b 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -42,6 +42,12 @@ android:defaultValue="false" /> +