From 4539f11f8b0590004b0c0463b7f3179119234c78 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:54:34 +0000 Subject: [PATCH 01/27] perf: deduplicate WebRTC stats polling --- src/webrtc/index.ts | 50 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts index a7c75566..926ddc6f 100644 --- a/src/webrtc/index.ts +++ b/src/webrtc/index.ts @@ -116,6 +116,11 @@ class webRTCClient { _audioLevel = 0; _audioEnergySnapshot: AudioEnergySnapshot | null = null; _hasAudioLevelSample = false; + // Compartilha getStats() entre os timers de áudio e vídeo. + _statsCache: any = null; + _statsCacheAt = 0; + _statsRequest: Promise | null = null; + _statsCacheTtlMs = 250; _hasVideoTrack = false; _hasAudioTrack = false; _hasSentInitialVideoKeyframeRequest = false; @@ -311,6 +316,9 @@ class webRTCClient { this._isResetting = true; this._webrtcClient?.close(); this._resetAudioLevelTracking(); + this._clearStatsCache(); + globalThis._lastStat = null; + this._webrtcClient = undefined; this._resetVideoTrackState(); for (const name in this._webrtcChannelProcessors) { @@ -530,13 +538,44 @@ class webRTCClient { this._gpState = gpState; } + _getStatsCached() { + const now = Date.now(); + if (this._statsCache && now - this._statsCacheAt < this._statsCacheTtlMs) { + return Promise.resolve(this._statsCache); + } + if (this._statsRequest) { + return this._statsRequest; + } + if (!this._webrtcClient) { + return Promise.resolve(null); + } + this._statsRequest = this._webrtcClient + .getStats() + .then(stats => { + this._statsCache = stats; + this._statsCacheAt = Date.now(); + return stats; + }) + .finally(() => { + this._statsRequest = null; + }); + return this._statsRequest; + } + _clearStatsCache() { + this._statsCache = null; + this._statsCacheAt = 0; + this._statsRequest = null; + } getAudioVolume() { return new Promise(resolve => { let volume = 0; if (this._webrtcClient) { - this._webrtcClient - .getStats() + this._getStatsCached() .then(stats => { + if (!stats) { + resolve(volume); + return; + } stats.forEach((stat: any) => { if ( stat.type === 'inbound-rtp' && @@ -575,9 +614,12 @@ class webRTCClient { decode: '', }; if (this._webrtcClient) { - this._webrtcClient - .getStats() + this._getStatsCached() .then(stats => { + if (!stats) { + resove(performances); + return; + } stats.forEach((stat: any) => { if ( stat.type === 'inbound-rtp' && From 26785170faec4b3ff9544165294a72befe560c0a Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:08:07 +0000 Subject: [PATCH 02/27] perf: reduce auxiliary stream polling --- src/pages/NativeStream.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index 13ddcf38..a4f87b8b 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -1394,7 +1394,7 @@ export function NativeStreamScreenBase({ ); } }); - }, 16); + }, 50); } } else if (state === CLOSED) { if (isRequestExit.current) { @@ -1889,7 +1889,8 @@ export function NativeStreamScreenBase({ }; updatePerformance(); - performanceInterval.current = setInterval(updatePerformance, 1000); + // A telemetria é auxiliar; 2 Hz reduz o trabalho no thread JS durante o jogo. + performanceInterval.current = setInterval(updatePerformance, 2000); return () => { if (performanceInterval.current) { From af83ddd282e0a38c9dd5eb368cee634a8e62cdc6 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:10:11 +0000 Subject: [PATCH 03/27] chore: translate optimization comments to English --- src/pages/NativeStream.tsx | 2 +- src/webrtc/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index a4f87b8b..3b05e366 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -1889,7 +1889,7 @@ export function NativeStreamScreenBase({ }; updatePerformance(); - // A telemetria é auxiliar; 2 Hz reduz o trabalho no thread JS durante o jogo. + // Telemetry is auxiliary; 2 Hz reduces JS-thread work during gameplay. performanceInterval.current = setInterval(updatePerformance, 2000); return () => { diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts index 926ddc6f..9869bf9c 100644 --- a/src/webrtc/index.ts +++ b/src/webrtc/index.ts @@ -116,7 +116,7 @@ class webRTCClient { _audioLevel = 0; _audioEnergySnapshot: AudioEnergySnapshot | null = null; _hasAudioLevelSample = false; - // Compartilha getStats() entre os timers de áudio e vídeo. + // Share getStats() between the audio and video timers. _statsCache: any = null; _statsCacheAt = 0; _statsRequest: Promise | null = null; From e565c115e164700841b426817e8cf63c622170ad Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:12:37 +0000 Subject: [PATCH 04/27] perf: pause performance polling in background --- src/pages/NativeStream.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index 3b05e366..c59e3a11 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -226,6 +226,7 @@ export function NativeStreamScreenBase({ React.useState(false); const [openMicro, setOpenMicro] = React.useState(false); const [isInPictureInPicture, setIsInPictureInPicture] = React.useState(false); + const [appState, setAppState] = React.useState(AppState.currentState); const xHomeApiRef = React.useRef(undefined); const xCloudApiRef = React.useRef(undefined); const isRumbling = React.useRef(false); @@ -754,6 +755,7 @@ export function NativeStreamScreenBase({ appStateSubscription.current = AppState.addEventListener( 'change', async state => { + setAppState(state); if ( !portraitMode && state === 'background' && @@ -1868,6 +1870,7 @@ export function NativeStreamScreenBase({ React.useEffect(() => { if ( connectState !== CONNECTED || + appState !== 'active' || !showPerformance || !webrtcClient || typeof webrtcClient.getStreamState !== 'function' @@ -1898,7 +1901,7 @@ export function NativeStreamScreenBase({ performanceInterval.current = null; } }; - }, [connectState, showPerformance, webrtcClient]); + }, [appState, connectState, showPerformance, webrtcClient]); const handlePowerOff = React.useCallback(async () => { const webApi = new WebApi(webToken); From 0c0ce7dc9fafa0556e129d3aacb44e06608642f5 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:15:08 +0000 Subject: [PATCH 05/27] perf: disable verbose input logs in production --- src/webrtc/Channel/Input.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/webrtc/Channel/Input.ts b/src/webrtc/Channel/Input.ts index 3bdb951f..25e220fb 100644 --- a/src/webrtc/Channel/Input.ts +++ b/src/webrtc/Channel/Input.ts @@ -132,10 +132,12 @@ export default class InputChannel extends BaseChannel { } onMessage(event: any) { - console.log( - 'Channel/Input.ts - [' + this._channelName + '] onMessage:', - event, - ); + if (__DEV__) { + console.log( + 'Channel/Input.ts - [' + this._channelName + '] onMessage:', + event, + ); + } const dataView = new DataView(event.data); @@ -189,10 +191,12 @@ export default class InputChannel extends BaseChannel { clearInterval(this._inputInterval); super.onClose(event); - console.log( - 'Channel/Input.ts - [' + this._channelName + '] onClose:', - event, - ); + if (__DEV__) { + console.log( + 'Channel/Input.ts - [' + this._channelName + '] onClose:', + event, + ); + } } getGamepadQueue(size = 30) { From e906ccaa7b38bda038b6c23f1a0946879f1d2694 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:16:35 +0000 Subject: [PATCH 06/27] perf: bound processed frame metadata queue --- src/webrtc/Channel/Input.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/webrtc/Channel/Input.ts b/src/webrtc/Channel/Input.ts index 25e220fb..cdf4d5d3 100644 --- a/src/webrtc/Channel/Input.ts +++ b/src/webrtc/Channel/Input.ts @@ -60,6 +60,8 @@ export interface KeyboardFrame { key: string; } +const MAX_FRAME_METADATA_QUEUE = 60; + export default class InputChannel extends BaseChannel { _inputSequenceNum = 0; @@ -189,8 +191,12 @@ export default class InputChannel extends BaseChannel { onClose(event: any) { clearInterval(this._inputInterval); - + this._inputInterval = null; + this._frameMetadataQueue.length = 0; + this._gamepadFrames.length = 0; + this._pointerFrames.length = 0; super.onClose(event); + if (__DEV__) { console.log( 'Channel/Input.ts - [' + this._channelName + '] onClose:', @@ -263,14 +269,22 @@ export default class InputChannel extends BaseChannel { destroy() { clearInterval(this._inputInterval); + this._inputInterval = null; + this._frameMetadataQueue.length = 0; + this._gamepadFrames.length = 0; + this._pointerFrames.length = 0; super.destroy(); } - addProcessedFrame(frame: any) { frame.frameRenderedTimeMs = performance.now(); + if (this._frameMetadataQueue.length >= MAX_FRAME_METADATA_QUEUE) { + // Drop stale feedback instead of allowing an unbounded backlog. + this._frameMetadataQueue.shift(); + } this._frameMetadataQueue.push(frame); } + getMetadataQueue(size = 30) { return this._frameMetadataQueue.splice(0, size - 1); } From 5b4dd3e09d32774e5b0fd3c32151c5e4820e0d22 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:18:26 +0000 Subject: [PATCH 07/27] perf: remove redundant orientation timers --- src/pages/NativeStream.tsx | 6 ------ src/pages/Stream.tsx | 11 ++--------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index c59e3a11..512eec04 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -306,22 +306,16 @@ export function NativeStreamScreenBase({ ); React.useEffect(() => { - let layoutTimer: any = null; const lockTimer = setTimeout(() => { if (portraitMode) { Orientation.lockToPortrait(); } else { Orientation.lockToLandscape(); } - - layoutTimer = setTimeout(() => {}, 100); }, 500); return () => { clearTimeout(lockTimer); - if (layoutTimer) { - clearTimeout(layoutTimer); - } Orientation.unlockAllOrientations(); }; }, [route.params?.sessionId, route.params?.streamType, portraitMode]); diff --git a/src/pages/Stream.tsx b/src/pages/Stream.tsx index 05b6d1f6..dafc1a47 100644 --- a/src/pages/Stream.tsx +++ b/src/pages/Stream.tsx @@ -151,21 +151,14 @@ function StreamScreen({navigation, route}) { const webviewRef = React.useRef(null); React.useEffect(() => { - let layoutTimer: any = null; const lockTimer = setTimeout(() => { Orientation.lockToLandscape(); - - layoutTimer = setTimeout(() => { - const {height: dHeight} = Dimensions.get('window'); - setModalMaxHeight(dHeight - 50); - }, 100); + const {height: dHeight} = Dimensions.get('window'); + setModalMaxHeight(dHeight - 50); }, 500); return () => { clearTimeout(lockTimer); - if (layoutTimer) { - clearTimeout(layoutTimer); - } Orientation.unlockAllOrientations(); }; }, [route.params?.sessionId, route.params?.streamType]); From 2cfa8f1ac26d5ed686cfcdafd2eaee93518fcd1d Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:21:13 +0000 Subject: [PATCH 08/27] perf: avoid typed array allocations in input packets --- src/webrtc/Packet/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/webrtc/Packet/index.ts b/src/webrtc/Packet/index.ts index e335bee4..ae2ca966 100644 --- a/src/webrtc/Packet/index.ts +++ b/src/webrtc/Packet/index.ts @@ -400,13 +400,11 @@ export default class InputPacket { } _convertToInt16(e: any) { - const int = new Int16Array(1); - return (int[0] = e), int[0]; + return (e << 16) >> 16; } _convertToUInt16(e: any) { - const int = new Uint16Array(1); - return (int[0] = e), int[0]; + return e & 0xffff; } _clampUint8(value: number) { From e8f65e9da373a70d0aa9e4679485dd6fdc7167b3 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:26:43 +0000 Subject: [PATCH 09/27] perf: avoid redundant FSR renderer updates --- .../oney/WebRTCModule/RTCFsrVideoView.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java index ef10e2c8..d295c86b 100644 --- a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java +++ b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java @@ -63,6 +63,9 @@ public class RTCFsrVideoView extends ViewGroup { private boolean fsrEnabled = true; private float fsrSharpness = 2f; private boolean fsrInitialized; + private float appliedFsrSharpness = Float.NaN; + private int appliedSurfaceWidth = -1; + private int appliedSurfaceHeight = -1; private Object fsrDrawerProxy; private Object fallbackDrawer; @@ -494,6 +497,7 @@ public void setFsrEnabled(boolean enabled) { public void setFsrSharpness(float sharpness) { fsrSharpness = sharpness; + appliedFsrSharpness = Float.NaN; fsrVideoProcessor.setSharpness(sharpness / 10f); } @@ -652,11 +656,18 @@ private void handleDrawOes(Object[] args) { } ensureFsrInitialized(); - fsrVideoProcessor.setSharpness(fsrSharpness / 10f); - if (viewportWidth > 0 && viewportHeight > 0) { - fsrVideoProcessor.setSurfaceSize(viewportWidth, viewportHeight); - } else if (frameWidth > 0 && frameHeight > 0) { - fsrVideoProcessor.setSurfaceSize(frameWidth, frameHeight); + float requestedSharpness = fsrSharpness / 10f; + if (Float.compare(appliedFsrSharpness, requestedSharpness) != 0) { + fsrVideoProcessor.setSharpness(requestedSharpness); + appliedFsrSharpness = requestedSharpness; + } + int surfaceWidth = viewportWidth > 0 ? viewportWidth : frameWidth; + int surfaceHeight = viewportHeight > 0 ? viewportHeight : frameHeight; + if (surfaceWidth > 0 && surfaceHeight > 0 + && (surfaceWidth != appliedSurfaceWidth || surfaceHeight != appliedSurfaceHeight)) { + fsrVideoProcessor.setSurfaceSize(surfaceWidth, surfaceHeight); + appliedSurfaceWidth = surfaceWidth; + appliedSurfaceHeight = surfaceHeight; } boolean rendered = fsrVideoProcessor.draw( @@ -772,9 +783,12 @@ private synchronized void releaseDrawerResources() { Log.w(TAG, "Failed to release FSR processor", t); } fsrInitialized = false; + appliedFsrSharpness = Float.NaN; + appliedSurfaceWidth = -1; + appliedSurfaceHeight = -1; } - if (fallbackDrawer != null) { + try { Method release = findByNameAndArgCount(fallbackDrawer.getClass(), "release", 0); if (release != null) { From 2e6f832e3efe01333c3b6d767b31315e3dfdb09f Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:28:50 +0000 Subject: [PATCH 10/27] perf: reduce verbose FSR logging --- .../com/xstreaming/fsr/FsrVideoProcessor.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java index 9f5cdb05..f6d86894 100644 --- a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java +++ b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java @@ -94,9 +94,6 @@ public void initialize(int glMajorVersion, int glMinorVersion, String extensions Log.w(TAG, "GLES3 context without GL_OES_EGL_image_external_essl3, force FSR 2.0 shaders"); } - Log.i(TAG, "FSR preferred shader dir: " + preferredDir); - Log.i(TAG, "OpenGL extensions: " + extensions); - boolean skipTwoPassForDriverStability = !ENABLE_TWO_PASS_PIPELINE; if (skipTwoPassForDriverStability) { Log.w(TAG, "Skip two-pass FSR globally, use mobile pipeline for stability"); @@ -149,7 +146,6 @@ public void setSurfaceSize(int width, int height) { if (outputWidth == width && outputHeight == height) { return; } - Log.i(TAG, "setSurfaceSize(" + width + "," + height + ")"); outputWidth = width; outputHeight = height; outputSize = new float[]{width, height}; @@ -190,12 +186,6 @@ public void release() { public void setHdrToneMappingEnabled(boolean enabled) { hdrInputEnabled = enabled; - if (enabled && !FORCE_SOFTWARE_HDR_TONE_MAP) { - Log.i( - TAG, - "HDR stream detected; software HDR tone-map disabled." - ); - } } public void setSharpness(float value) { @@ -203,13 +193,6 @@ public void setSharpness(float value) { mobileSharpness = clamped; // Map [0..2] (stronger as larger) to RCAS stop domain [2..0]. rcasSharpness = 2f - clamped; - Log.i( - TAG, - "Sharpness request=" + value + ", clamped=" + clamped - + ", mobileApplied=" + mobileSharpness - + ", rcasApplied=" + rcasSharpness - ); - logEffectiveSharpness("setSharpness"); } public void resetSharpness() { From c98473eb0194238d0b5b202f6f29036525c4af74 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:30:44 +0000 Subject: [PATCH 11/27] perf: reuse FSR texture size buffer --- .../com/xstreaming/fsr/FsrVideoProcessor.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java index f6d86894..90e4291e 100644 --- a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java +++ b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java @@ -66,6 +66,9 @@ public class FsrVideoProcessor implements VideoProcessor { private int outputWidth = -1; private int outputHeight = -1; private float[] outputSize = new float[2]; + private final float[] inputTextureSize = new float[2]; + private int inputTextureWidth = -1; + private int inputTextureHeight = -1; public FsrVideoProcessor(Context context) { this.context = context.getApplicationContext(); @@ -330,17 +333,18 @@ private boolean drawTwoPass(int frameTexture, } } - float[] inputTextureSize = null; - if (needInputSize) { - inputTextureSize = (frameWidth > 0 && frameHeight > 0) - ? new float[]{frameWidth, frameHeight} - : new float[]{0f, 0f}; + if (needInputSize + && (frameWidth != inputTextureWidth || frameHeight != inputTextureHeight)) { + inputTextureSize[0] = frameWidth > 0 ? frameWidth : 0f; + inputTextureSize[1] = frameHeight > 0 ? frameHeight : 0f; + inputTextureWidth = frameWidth; + inputTextureHeight = frameHeight; } GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]); try { easu.setSamplerTexIdUniform("inputTexture", frameTexture, 0); - if (inputTextureSize != null) { + if (needInputSize) { easu.setFloatsUniform("inputTextureSize", inputTextureSize); } easu.setFloatsUniform("outputTextureSize", outputSize); @@ -408,16 +412,17 @@ private boolean drawMobileSinglePass(int frameTexture, return drawPassthrough(frameTexture, transformMatrix); } - float[] inputTextureSize = null; - if (needInputSize) { - inputTextureSize = (frameWidth > 0 && frameHeight > 0) - ? new float[]{frameWidth, frameHeight} - : new float[]{0f, 0f}; + if (needInputSize + && (frameWidth != inputTextureWidth || frameHeight != inputTextureHeight)) { + inputTextureSize[0] = frameWidth > 0 ? frameWidth : 0f; + inputTextureSize[1] = frameHeight > 0 ? frameHeight : 0f; + inputTextureWidth = frameWidth; + inputTextureHeight = frameHeight; } try { program.setSamplerTexIdUniform("inputTexture", frameTexture, 0); - if (inputTextureSize != null) { + if (needInputSize) { program.setFloatsUniform("inputTextureSize", inputTextureSize); } program.setFloatsUniform("outputTextureSize", outputSize); From 5c20b9564d887622a9c10217a7e42b4424ac80f0 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:33:15 +0000 Subject: [PATCH 12/27] fix: persist FSR fallback after draw failure --- .../java/com/oney/WebRTCModule/RTCFsrVideoView.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java index d295c86b..32850603 100644 --- a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java +++ b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java @@ -61,6 +61,7 @@ public class RTCFsrVideoView extends ViewGroup { private VideoTrack videoTrack; private boolean fsrEnabled = true; + private boolean fsrFallbackActive; private float fsrSharpness = 2f; private boolean fsrInitialized; private float appliedFsrSharpness = Float.NaN; @@ -492,6 +493,9 @@ public void setZOrder(int zOrder) { public void setFsrEnabled(boolean enabled) { fsrEnabled = enabled; + if (enabled) { + fsrFallbackActive = false; + } fsrVideoProcessor.setFsrEnabled(enabled); } @@ -631,7 +635,7 @@ private void handleDrawOes(Object[] args) { return; } - if (!fsrEnabled) { + if (!fsrEnabled || fsrFallbackActive) { invokeFallbackDrawer("drawOes", args); return; } @@ -678,10 +682,13 @@ private void handleDrawOes(Object[] args) { texMatrix ); if (!rendered) { + // Keep using the stable fallback after a failed FSR draw. + fsrFallbackActive = true; invokeFallbackDrawer("drawOes", args); } } catch (Throwable t) { Log.e(TAG, "FSR draw failed, fallback to default OES drawer.", t); + fsrFallbackActive = true; invokeFallbackDrawer("drawOes", args); } } @@ -783,6 +790,7 @@ private synchronized void releaseDrawerResources() { Log.w(TAG, "Failed to release FSR processor", t); } fsrInitialized = false; + fsrFallbackActive = false; appliedFsrSharpness = Float.NaN; appliedSurfaceWidth = -1; appliedSurfaceHeight = -1; From ef128ca9173ee49f5a866d4299aa6573387f632d Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:35:10 +0000 Subject: [PATCH 13/27] perf: cache fallback drawer reflection methods --- .../oney/WebRTCModule/RTCFsrVideoView.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java index 32850603..af986782 100644 --- a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java +++ b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java @@ -69,6 +69,8 @@ public class RTCFsrVideoView extends ViewGroup { private int appliedSurfaceHeight = -1; private Object fsrDrawerProxy; private Object fallbackDrawer; + private Method fallbackDrawOesMethod; + private Method fallbackReleaseMethod; private final FsrVideoProcessor fsrVideoProcessor; @@ -761,13 +763,21 @@ private void invokeFallbackDrawer(String methodName, Object[] args) { if (drawer == null) { return; } - try { - Method target = findByNameAndArgCount(drawer.getClass(), methodName, args == null ? 0 : args.length); + Method target; + if ("drawOes".equals(methodName)) { + if (fallbackDrawOesMethod == null) { + fallbackDrawOesMethod = findByNameAndArgCount(drawer.getClass(), methodName, args == null ? 0 : args.length); + } + target = fallbackDrawOesMethod; + } else { + target = findByNameAndArgCount(drawer.getClass(), methodName, args == null ? 0 : args.length); + } if (target == null) { return; } target.invoke(drawer, args == null ? new Object[0] : args); + } catch (Throwable t) { Log.e(TAG, "Fallback drawer invocation failed: " + methodName, t); } @@ -796,16 +806,20 @@ private synchronized void releaseDrawerResources() { appliedSurfaceHeight = -1; } if (fallbackDrawer != null) { - try { - Method release = findByNameAndArgCount(fallbackDrawer.getClass(), "release", 0); - if (release != null) { - release.invoke(fallbackDrawer); + if (fallbackReleaseMethod == null) { + fallbackReleaseMethod = findByNameAndArgCount(fallbackDrawer.getClass(), "release", 0); } + if (fallbackReleaseMethod != null) { + fallbackReleaseMethod.invoke(fallbackDrawer); + } + } catch (Throwable t) { Log.w(TAG, "Failed to release fallback drawer", t); } fallbackDrawer = null; + fallbackDrawOesMethod = null; + fallbackReleaseMethod = null; } } } From d58153a0c8409abaeb064b9a2e057f79768faa34 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:37:25 +0000 Subject: [PATCH 14/27] perf: reuse FSR output size buffer --- .../src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java index 90e4291e..17406e63 100644 --- a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java +++ b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java @@ -65,7 +65,7 @@ public class FsrVideoProcessor implements VideoProcessor { private int outputWidth = -1; private int outputHeight = -1; - private float[] outputSize = new float[2]; + private final float[] outputSize = new float[2]; private final float[] inputTextureSize = new float[2]; private int inputTextureWidth = -1; private int inputTextureHeight = -1; @@ -151,7 +151,8 @@ public void setSurfaceSize(int width, int height) { } outputWidth = width; outputHeight = height; - outputSize = new float[]{width, height}; + outputSize[0] = width; + outputSize[1] = height; if (pipelineMode == PIPELINE_TWO_PASS) { deleteFramebuffer(); From 46c18706e21dac58e2657b69b1f5f80904b0c549 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:38:58 +0000 Subject: [PATCH 15/27] perf: cache HDR tone mapping decision --- .../com/xstreaming/fsr/FsrVideoProcessor.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java index 17406e63..7db2028d 100644 --- a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java +++ b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java @@ -62,6 +62,7 @@ public class FsrVideoProcessor implements VideoProcessor { private boolean fsrEnabled = true; private boolean hdrInputEnabled; private boolean usingPqWindow; + private boolean softwareHdrToneMap; private int outputWidth = -1; private int outputHeight = -1; @@ -190,6 +191,7 @@ public void release() { public void setHdrToneMappingEnabled(boolean enabled) { hdrInputEnabled = enabled; + updateSoftwareHdrToneMapState(); } public void setSharpness(float value) { @@ -627,18 +629,16 @@ private void logEffectiveSharpness(String reason) { } private boolean shouldApplySoftwareHdrToneMap() { - if (!hdrInputEnabled) { - return false; - } - if (usingPqWindow) { - // PQ output window should preserve HDR signal and avoid SDR tone map in this shader path. - return false; - } - return FORCE_SOFTWARE_HDR_TONE_MAP; + return softwareHdrToneMap; + } + + private void updateSoftwareHdrToneMapState() { + softwareHdrToneMap = hdrInputEnabled && !usingPqWindow && FORCE_SOFTWARE_HDR_TONE_MAP; } private void detectHdrWindowState() { usingPqWindow = false; + updateSoftwareHdrToneMapState(); android.opengl.EGLDisplay display = EGL14.eglGetCurrentDisplay(); android.opengl.EGLSurface drawSurface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW); if (display == null || display == EGL14.EGL_NO_DISPLAY @@ -659,6 +659,7 @@ private void detectHdrWindowState() { if (EGL14.eglQuerySurface(display, drawSurface, EGL_GL_COLORSPACE_KHR, colorspace, 0)) { usingPqWindow = colorspace[0] == EGL_GL_COLORSPACE_BT2020_PQ_EXT; } + updateSoftwareHdrToneMapState(); Log.i( TAG, From fa92b42ef88eaba05eca8d6c9d1b140ad5e4918f Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:54:45 +0000 Subject: [PATCH 16/27] perf: disable FSR on low-memory devices --- .../oney/WebRTCModule/RTCFsrVideoView.java | 25 +++++++++++++++++-- .../WebRTCModule/RTCFsrVideoViewManager.java | 5 ++++ src/components/RTCFsrView.tsx | 1 + src/pages/NativeStream.tsx | 1 + 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java index af986782..5c6e39e9 100644 --- a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java +++ b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java @@ -1,6 +1,7 @@ package com.oney.WebRTCModule; import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.content.Context; import android.graphics.Color; import android.graphics.Point; @@ -61,6 +62,7 @@ public class RTCFsrVideoView extends ViewGroup { private VideoTrack videoTrack; private boolean fsrEnabled = true; + private boolean autoDisableFsrOnLowMemory; private boolean fsrFallbackActive; private float fsrSharpness = 2f; private boolean fsrInitialized; @@ -498,7 +500,26 @@ public void setFsrEnabled(boolean enabled) { if (enabled) { fsrFallbackActive = false; } - fsrVideoProcessor.setFsrEnabled(enabled); + applyEffectiveFsrEnabled(); + } + + public void setAutoDisableFsrOnLowMemory(boolean enabled) { + autoDisableFsrOnLowMemory = enabled; + applyEffectiveFsrEnabled(); + } + + private void applyEffectiveFsrEnabled() { + boolean lowMemoryDevice = false; + if (autoDisableFsrOnLowMemory) { + ActivityManager activityManager = + (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE); + lowMemoryDevice = activityManager != null && activityManager.isLowRamDevice(); + } + boolean effectiveEnabled = fsrEnabled && !lowMemoryDevice; + if (effectiveEnabled) { + fsrFallbackActive = false; + } + fsrVideoProcessor.setFsrEnabled(effectiveEnabled); } public void setFsrSharpness(float sharpness) { @@ -731,7 +752,7 @@ private synchronized void ensureFsrInitialized() { String extensions = GLES20.glGetString(GLES20.GL_EXTENSIONS); fsrVideoProcessor.initialize(major, minor, extensions == null ? "" : extensions); - fsrVideoProcessor.setFsrEnabled(fsrEnabled); + applyEffectiveFsrEnabled(); fsrVideoProcessor.setSharpness(fsrSharpness / 10f); fsrInitialized = true; } diff --git a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoViewManager.java b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoViewManager.java index 4ad18b31..acd3f41e 100644 --- a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoViewManager.java +++ b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoViewManager.java @@ -47,6 +47,11 @@ public void setFsrEnabled(RTCFsrVideoView view, boolean enabled) { view.setFsrEnabled(enabled); } + @ReactProp(name = "autoDisableFsrOnLowMemory", defaultBoolean = false) + public void setAutoDisableFsrOnLowMemory(RTCFsrVideoView view, boolean enabled) { + view.setAutoDisableFsrOnLowMemory(enabled); + } + @ReactProp(name = "fsrSharpness", defaultFloat = 2f) public void setFsrSharpness(RTCFsrVideoView view, float sharpness) { view.setFsrSharpness(sharpness); diff --git a/src/components/RTCFsrView.tsx b/src/components/RTCFsrView.tsx index 8e1db6fc..db20216f 100644 --- a/src/components/RTCFsrView.tsx +++ b/src/components/RTCFsrView.tsx @@ -14,6 +14,7 @@ type Props = ViewProps & { zOrder?: number; videoFormat?: string; fsrEnabled?: boolean; + autoDisableFsrOnLowMemory?: boolean; fsrSharpness?: number; }; diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index 512eec04..d6d7e2cd 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -2569,6 +2569,7 @@ export function NativeStreamScreenBase({ streamURL={remote} videoFormat={video_format || ''} fsrEnabled={true} + autoDisableFsrOnLowMemory={true} fsrSharpness={fsrSharpness} /> Date: Sat, 29 Aug 2026 19:58:19 +0000 Subject: [PATCH 17/27] fix: release FSR resources on fallback --- .../src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java index 5c6e39e9..7ac3a8da 100644 --- a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java +++ b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java @@ -705,12 +705,14 @@ private void handleDrawOes(Object[] args) { texMatrix ); if (!rendered) { - // Keep using the stable fallback after a failed FSR draw. + // Release FSR resources and keep using the stable fallback after a failed draw. + releaseDrawerResources(); fsrFallbackActive = true; invokeFallbackDrawer("drawOes", args); } } catch (Throwable t) { Log.e(TAG, "FSR draw failed, fallback to default OES drawer.", t); + releaseDrawerResources(); fsrFallbackActive = true; invokeFallbackDrawer("drawOes", args); } From 73cd299ffceb34a4823d698a9106d72fcf955e6f Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:05:33 +0000 Subject: [PATCH 18/27] fix: sequence video renderer cleanup --- .../oney/WebRTCModule/RTCFsrVideoView.java | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java index 7ac3a8da..9fa70b3e 100644 --- a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java +++ b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java @@ -53,6 +53,7 @@ public class RTCFsrVideoView extends ViewGroup { private int frameWidth; private boolean mirror; private boolean rendererAttached; + private boolean rendererCleanupInProgress; private ScalingType scalingType; private int videoFormatMode = VIDEO_FORMAT_MODE_AUTO; private float videoFormatAspectRatio; @@ -241,29 +242,40 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) { } private void removeRendererFromVideoTrack() { - if (rendererAttached) { - if (videoTrack != null) { - ThreadUtils.runOnExecutor(() -> { - try { - videoTrack.removeSink(surfaceViewRenderer); - } catch (Throwable ignored) { - // Ignore track lifecycle race. + if (!rendererAttached || rendererCleanupInProgress) { + return; + } + + rendererAttached = false; + rendererCleanupInProgress = true; + VideoTrack track = videoTrack; + ThreadUtils.runOnExecutor(() -> { + try { + if (track != null) { + track.removeSink(surfaceViewRenderer); + } + } catch (Throwable ignored) { + // Ignore track lifecycle race. + } finally { + surfaceViewRenderer.release(); + releaseDrawerResources(); + if (surfaceViewRendererInstances > 0) { + surfaceViewRendererInstances--; + } + post(() -> { + rendererCleanupInProgress = false; + synchronized (layoutSyncRoot) { + frameHeight = 0; + frameRotation = 0; + frameWidth = 0; + } + requestSurfaceViewRendererLayout(); + if (videoTrack != null && ViewCompat.isAttachedToWindow(this)) { + tryAddRendererToVideoTrack(); } }); } - - surfaceViewRenderer.release(); - releaseDrawerResources(); - surfaceViewRendererInstances--; - rendererAttached = false; - - synchronized (layoutSyncRoot) { - frameHeight = 0; - frameRotation = 0; - frameWidth = 0; - } - requestSurfaceViewRendererLayout(); - } + }); } @SuppressLint("WrongCall") @@ -529,7 +541,8 @@ public void setFsrSharpness(float sharpness) { } private void tryAddRendererToVideoTrack() { - if (!rendererAttached && videoTrack != null && ViewCompat.isAttachedToWindow(this)) { + if (!rendererAttached && !rendererCleanupInProgress + && videoTrack != null && ViewCompat.isAttachedToWindow(this)) { EglBase.Context sharedContext = EglUtils.getRootEglBaseContext(); if (sharedContext == null) { From f2a97c1a61a55813468a16dbb0546f5d017a62f7 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:06:24 +0000 Subject: [PATCH 19/27] perf: reduce renderer and overlay overhead --- .../com/xstreaming/fsr/FsrVideoProcessor.java | 7 ++++- src/pages/NativeStream.tsx | 28 +++++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java index 7db2028d..42ff3136 100644 --- a/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java +++ b/android/app/src/main/java/com/xstreaming/fsr/FsrVideoProcessor.java @@ -58,6 +58,7 @@ public class FsrVideoProcessor implements VideoProcessor { private boolean mobileHasSharpness; private boolean mobileHasHdrToneMap; private boolean twoPassFailureLogged; + private long lastGlErrorLogTimestamp; private boolean fsrEnabled = true; private boolean hdrInputEnabled; @@ -603,7 +604,11 @@ private boolean checkGlError(String message) { GlUtil.checkGlError(); return true; } catch (GlException e) { - Log.e(TAG, message, e); + long now = System.currentTimeMillis(); + if (now - lastGlErrorLogTimestamp >= 5000L) { + lastGlErrorLogTimestamp = now; + Log.e(TAG, message, e); + } return false; } } diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index d6d7e2cd..2055e5cc 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -1880,7 +1880,21 @@ export function NativeStreamScreenBase({ webrtcClient .getStreamState() .then(res => { - setPerformance(res); + setPerformance(previous => { + if ( + previous.resolution === res.resolution && + previous.rtt === res.rtt && + previous.jit === res.jit && + previous.fps === res.fps && + previous.pl === res.pl && + previous.fl === res.fl && + previous.br === res.br && + previous.decode === res.decode + ) { + return previous; + } + return res; + }); }) .catch(() => {}); }; @@ -2572,11 +2586,13 @@ export function NativeStreamScreenBase({ autoDisableFsrOnLowMemory={true} fsrSharpness={fsrSharpness} /> - + {settings.native_touch && !isInPictureInPicture ? ( + + ) : null} ); } From 2eaf6327c0edbde8936d8d0b8783c2aa9b58fe64 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:10:11 +0000 Subject: [PATCH 20/27] perf: prevent overlapping performance polls --- src/pages/NativeStream.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index 2055e5cc..66733001 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -242,6 +242,7 @@ export function NativeStreamScreenBase({ const audioGainRef = React.useRef(1); const keepaliveInterval = React.useRef(null); const performanceInterval = React.useRef(null); + const performanceRequestInFlight = React.useRef(false); const connectStateRef = React.useRef(''); const gpDownEventListener = React.useRef(undefined); @@ -1877,6 +1878,10 @@ export function NativeStreamScreenBase({ } const updatePerformance = () => { + if (performanceRequestInFlight.current) { + return; + } + performanceRequestInFlight.current = true; webrtcClient .getStreamState() .then(res => { @@ -1896,7 +1901,10 @@ export function NativeStreamScreenBase({ return res; }); }) - .catch(() => {}); + .catch(() => {}) + .finally(() => { + performanceRequestInFlight.current = false; + }); }; updatePerformance(); From 2335697e8e0f2e3ecc106d760f6974cf2b4078e6 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:11:23 +0000 Subject: [PATCH 21/27] perf: skip redundant video layout updates --- .../com/oney/WebRTCModule/RTCFsrVideoView.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java index 9fa70b3e..508ce67a 100644 --- a/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java +++ b/android/app/src/main/java/com/oney/WebRTCModule/RTCFsrVideoView.java @@ -54,6 +54,11 @@ public class RTCFsrVideoView extends ViewGroup { private boolean mirror; private boolean rendererAttached; private boolean rendererCleanupInProgress; + private boolean hasAppliedLayout; + private int appliedLayoutLeft; + private int appliedLayoutTop; + private int appliedLayoutRight; + private int appliedLayoutBottom; private ScalingType scalingType; private int videoFormatMode = VIDEO_FORMAT_MODE_AUTO; private float videoFormatAspectRatio; @@ -237,6 +242,18 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) { } } + if (hasAppliedLayout + && appliedLayoutLeft == l + && appliedLayoutTop == t + && appliedLayoutRight == r + && appliedLayoutBottom == b) { + return; + } + appliedLayoutLeft = l; + appliedLayoutTop = t; + appliedLayoutRight = r; + appliedLayoutBottom = b; + hasAppliedLayout = true; surfaceViewRenderer.layout(l, t, r, b); applyRendererLayoutAspectRatio(r - l, b - t); } @@ -269,6 +286,7 @@ private void removeRendererFromVideoTrack() { frameRotation = 0; frameWidth = 0; } + hasAppliedLayout = false; requestSurfaceViewRendererLayout(); if (videoTrack != null && ViewCompat.isAttachedToWindow(this)) { tryAddRendererToVideoTrack(); From 43d51824c164cb35364c6e6086088ec58fd1e4f1 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:13:13 +0000 Subject: [PATCH 22/27] perf: release WebRTC session state between streams --- src/webrtc/index.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts index 9869bf9c..afd49cb0 100644 --- a/src/webrtc/index.ts +++ b/src/webrtc/index.ts @@ -83,6 +83,7 @@ class webRTCClient { _webrtcDataChannels: any = {}; _webrtcChannelProcessors: any = {}; + _baseIceServerCount = this._webrtcConfiguration.iceServers.length; _isResetting = false; _gamepad_deadzone = 0.2; @@ -132,9 +133,16 @@ class webRTCClient { } init() { + this._isResetting = false; const settings = getSettings(); this._resetAudioLevelTracking(); this._resetVideoTrackState(); + this._iceCandidates.length = 0; + this._webrtcStates.iceCandidates = []; + this._webrtcConfiguration.iceServers = this._webrtcConfiguration.iceServers.slice( + 0, + this._baseIceServerCount, + ); // Use custom STUN/TURN server if ( @@ -320,6 +328,15 @@ class webRTCClient { globalThis._lastStat = null; this._webrtcClient = undefined; this._resetVideoTrackState(); + this._iceCandidates.length = 0; + this._webrtcStates.iceCandidates = []; + + for (const name in this._webrtcDataChannels) { + try { + this._webrtcDataChannels[name]?.close?.(); + } catch (_) {} + } + this._webrtcDataChannels = {}; for (const name in this._webrtcChannelProcessors) { this._webrtcChannelProcessors[name].destroy(); @@ -327,7 +344,8 @@ class webRTCClient { this._webrtcChannelProcessors = {}; - this._inputDriver.stop(); + this._inputDriver?.stop(); + this._inputDriver = undefined; } } From 546079f1d057681e9067c803a53fe2cb74b6b35f Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:14:57 +0000 Subject: [PATCH 23/27] perf: release WebRTC session callbacks --- src/webrtc/index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts index afd49cb0..69643d58 100644 --- a/src/webrtc/index.ts +++ b/src/webrtc/index.ts @@ -223,7 +223,7 @@ class webRTCClient { 'connectionstatechange:', this._webrtcClient?.connectionState, ); - this._connectedHandler(this._webrtcClient?.connectionState); + this._connectedHandler?.(this._webrtcClient?.connectionState); }); // this._webrtcClient.addEventListener('iceconnectionstatechange', _ => { @@ -346,6 +346,13 @@ class webRTCClient { this._inputDriver?.stop(); this._inputDriver = undefined; + this._trackHandler = undefined; + this._trackAddHandler = undefined; + this._connectedHandler = undefined; + this._sdpHandler = undefined; + this._rumbleHandler = undefined; + this._systemUiHandler = undefined; + this._messageHandler = undefined; } } @@ -384,7 +391,7 @@ class webRTCClient { _sdpHandler: any; sdpNegotiationChat() { this.createOffer().then(offer => { - this._sdpHandler(this, offer); + this._sdpHandler?.(this, offer); }); } From 1674460cbcf333580cb41a6ed6d796fc6633cc86 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:16:16 +0000 Subject: [PATCH 24/27] perf: release remote media tracks on cleanup --- src/pages/NativeStream.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index 66733001..f9f0f17a 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -1787,6 +1787,9 @@ export function NativeStreamScreenBase({ FullScreenManager.immersiveModeOff(); stopVibrate(); webrtcClient && webrtcClient.close(); + remoteStream.current?.getTracks?.().forEach(track => track.stop?.()); + remoteStream.current = null; + performanceRequestInFlight.current = false; usbGpEventListener.current && usbGpEventListener.current.remove(); gpDownEventListener.current && gpDownEventListener.current.remove(); gpUpEventListener.current && gpUpEventListener.current.remove(); From 6a2c966bb1865f6920fa9e031df601a995e101b9 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:18:42 +0000 Subject: [PATCH 25/27] perf: unmount inactive touch overlay --- src/pages/NativeStream.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index f9f0f17a..309cf97d 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -2617,11 +2617,13 @@ export function NativeStreamScreenBase({ streamURL={remote} videoFormat={video_format || ''} /> - + {settings.native_touch && !isInPictureInPicture ? ( + + ) : null} ); }; From 8c2b74fca5b78dcf187f29e153a8d5b09b2a5774 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:23:38 +0000 Subject: [PATCH 26/27] perf: disable mobile features on Android TV --- src/pages/NativeStream.tsx | 36 ++++++++++++++++++++---------------- src/pages/Stream.tsx | 29 ++++++++++++++++++----------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index 309cf97d..c0ddb479 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -55,6 +55,7 @@ const FAILED = 'failed'; const DUALSENSE = 'DualSenseController'; const LIVE_GAMEPAD_PROFILE = 'LiveLayout'; const PICTURE_IN_PICTURE_MODE_CHANGED = 'pictureInPictureModeChanged'; +const isAndroidTv = Platform.OS === 'android' && Platform.isTV === true; const { FullScreenManager, @@ -1087,7 +1088,7 @@ export function NativeStreamScreenBase({ } // Sensor - if (_settings.sensor) { + if (_settings.sensor && !isAndroidTv) { const sensorManager = _settings.sensor === 2 ? GamepadSensorModule : SensorModule; @@ -1331,7 +1332,7 @@ export function NativeStreamScreenBase({ ); // Alway show virtual gamepad - if (portraitMode || _settings.show_virtual_gamead) { + if (!isAndroidTv && (portraitMode || _settings.show_virtual_gamead)) { setShowVirtualGamepad(true); } @@ -2257,7 +2258,7 @@ export function NativeStreamScreenBase({ setShowVirtualGamepad(false); webrtcClient && webrtcClient.close(); setShowModal(false); - if (settings.sensor) { + if (settings.sensor && !isAndroidTv) { SensorModule.stopSensor(); GamepadSensorModule.stopSensor(); } @@ -2493,7 +2494,7 @@ export function NativeStreamScreenBase({ }, [openOptionsModal, showModal]); const renderVirtualGamepad = () => { - if (portraitMode) { + if (portraitMode || isAndroidTv) { return null; } if (isInPictureInPicture || !showVirtualGamepad) { @@ -2556,18 +2557,19 @@ export function NativeStreamScreenBase({ const useFsrRenderer = !!settings.fsr; const fsrSharpness = settings.fsr_display_options?.sharpness ?? 2; + const nativeTouchEnabled = !!settings.native_touch && !isAndroidTv; const handleNativePointerInput = React.useCallback( (event: PointerWireData) => { - if (!webrtcClient || !settings.native_touch) { + if (!webrtcClient || !nativeTouchEnabled) { return; } webrtcClient.getChannelProcessor('input')?.queuePointerInput([event]); }, - [settings.native_touch, webrtcClient], + [nativeTouchEnabled, webrtcClient], ); - const video_format = settings.native_touch ? '' : settings.video_format; + const video_format = nativeTouchEnabled ? '' : settings.video_format; const loadingPosterUrl = typeof route.params?.postUrl === 'string' ? route.params.postUrl : ''; const showLoadingPoster = loading && !!loadingPosterUrl; @@ -2597,7 +2599,7 @@ export function NativeStreamScreenBase({ autoDisableFsrOnLowMemory={true} fsrSharpness={fsrSharpness} /> - {settings.native_touch && !isInPictureInPicture ? ( + {nativeTouchEnabled && !isInPictureInPicture ? ( - {settings.native_touch && !isInPictureInPicture ? ( + {nativeTouchEnabled && !isInPictureInPicture ? ( { - if (!portraitMode || connectState !== CONNECTED) { + if (!portraitMode || isAndroidTv || connectState !== CONNECTED) { return null; } @@ -2756,12 +2758,14 @@ export function NativeStreamScreenBase({ {renderVirtualGamepad()} - setShowGamepadEditor(false)} - /> + {!isAndroidTv ? ( + setShowGamepadEditor(false)} + /> + ) : null} {renderMenu()} diff --git a/src/pages/Stream.tsx b/src/pages/Stream.tsx index dafc1a47..65f02c76 100644 --- a/src/pages/Stream.tsx +++ b/src/pages/Stream.tsx @@ -57,6 +57,7 @@ const CONNECTED = 'connected'; const DUALSENSE = 'DualSenseController'; const LIVE_GAMEPAD_PROFILE = 'LiveLayout'; const PICTURE_IN_PICTURE_MODE_CHANGED = 'pictureInPictureModeChanged'; +const isAndroidTv = Platform.OS === 'android' && Platform.isTV === true; const { FullScreenManager, @@ -567,7 +568,7 @@ function StreamScreen({navigation, route}) { }, 1000 / _settings.polling_rate); } - if (_settings.sensor) { + if (_settings.sensor && !isAndroidTv) { const sensorManager = _settings.sensor === 2 ? GamepadSensorModule : SensorModule; @@ -1417,6 +1418,9 @@ function StreamScreen({navigation, route}) { const isNativeLikeKernel = settings.gamepad_kernal === 'Native' || settings.gamepad_kernal === 'SDL'; // Close + if (isAndroidTv) { + return; + } if (showVirtualGamepad) { clearMacroTimers(); setShowVirtualGamepad(false); @@ -1439,10 +1443,11 @@ function StreamScreen({navigation, route}) { setShowVirtualGamepad(false); postData2Webview('disconnect', {}); setShowModal(false); - if (settings.sensor) { + if (settings.sensor && !isAndroidTv) { SensorModule.stopSensor(); GamepadSensorModule.stopSensor(); } + }; const background = { @@ -1498,7 +1503,7 @@ function StreamScreen({navigation, route}) { }; const renderVirtualGamepad = () => { - if (isInPictureInPicture || !showVirtualGamepad) { + if (isAndroidTv || isInPictureInPicture || !showVirtualGamepad) { return null; } const useCustomVirtualGamepad = settings.custom_virtual_gamepad !== ''; @@ -1538,14 +1543,16 @@ function StreamScreen({navigation, route}) { )} - {renderVirtualGamepad()} + {!isAndroidTv && renderVirtualGamepad()} - setShowGamepadEditor(false)} - /> + {!isAndroidTv ? ( + setShowGamepadEditor(false)} + /> + ) : null} { handleWebviewMessage(event); From 1f6ab72df3c7342c80187a137ecfc543a11bd2b7 Mon Sep 17 00:00:00 2001 From: gustvtech <1.11547125e+08+gustvtech@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:28:19 +0000 Subject: [PATCH 27/27] perf: release ICE candidates after negotiation --- src/pages/NativeStream.tsx | 1 + src/webrtc/index.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/pages/NativeStream.tsx b/src/pages/NativeStream.tsx index c0ddb479..a9f94745 100644 --- a/src/pages/NativeStream.tsx +++ b/src/pages/NativeStream.tsx @@ -1700,6 +1700,7 @@ export function NativeStreamScreenBase({ JSON.stringify(iceDetails), ); webrtcClient.setIceCandidates(iceDetails); + webrtcClient.clearIceCandidates?.(); setLoadingText(`${t('Exchange ICE successfully...')}`); }) .catch(e => { diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts index 69643d58..7cc36cfc 100644 --- a/src/webrtc/index.ts +++ b/src/webrtc/index.ts @@ -297,6 +297,11 @@ class webRTCClient { return this._iceCandidates; } + clearIceCandidates() { + this._iceCandidates.length = 0; + this._webrtcStates.iceCandidates = []; + } + setIceCandidates(iceDetails: any) { for (const candidate in iceDetails) { if (iceDetails[candidate].candidate === 'a=end-of-candidates') {