fix: bound stream size by what the client can actually decode - #62
Merged
tranvuongquocdat merged 1 commit intoSep 5, 2026
Conversation
Clients advertised their decoder's nominal `size` limit, which vendors routinely overstate — the real ceiling is the `blocks-per-second` budget. An Exynos HEVC decoder reports 8192x8192 while sustaining ~1.22M blocks/s, so the Mac's clamp never engaged: any resolution above ~1080p configured successfully and then decoded to a black screen. - Advertise the largest size the panel can show that MediaCodec's areSizeAndRateSupported() accepts at the panel's peak refresh rate. Falls back to the nominal limit only when panel geometry is unavailable, so devices that report nothing behave as before. - Raise MAX_FRAME_SIZE from 5MB to 32MB. A keyframe above 1080p at high bitrates exceeded it, and the overflow surfaced as an IOException that dropped the connection rather than a decode failure. - Transpose the reported ceiling when the capture's orientation differs from the client's, so a portrait display is not clamped against a landscape box. The limit stands for a macroblock area budget, which is indifferent to which side is longer. - Report the sustainable size in the decoder-stall toast. It used the nominal limit, so the one message shown when this failure happens told users "max ~8192x8192". - Require a Context in StreamClient so the panel probe cannot be silently skipped; the USB path was constructing it without one. Bounding the stream at the panel also makes HiDPI usable on a tablet: macOS renders at 2x, SCStream downsamples to the panel size before encode, and the client decodes a frame it can sustain.
|
@meta-boy is attempting to deploy a commit to the tranvuongquocdat2-7001's projects Team on Vercel. A member of the Team first needs to authorize it. |
tranvuongquocdat
added a commit
that referenced
this pull request
Sep 5, 2026
…c for faster streams The limit from #62 was measured at the panel's peak refresh rate, which shrank the picture on 120/144 Hz tablets even when the Mac streams at the 60 Hz default. The client now advertises the largest frame its decoder sustains at a fixed 60 fps reference; when the Mac streams faster it scales the box's area by 60/fps (blocks-per-second is linear in frame rate), so a 90/120 Hz session still lands inside the decoder's budget.
SirRiddle
pushed a commit
to SirRiddle/SideScreen
that referenced
this pull request
Sep 10, 2026
…c for faster streams The limit from tranvuongquocdat#62 was measured at the panel's peak refresh rate, which shrank the picture on 120/144 Hz tablets even when the Mac streams at the 60 Hz default. The client now advertises the largest frame its decoder sustains at a fixed 60 fps reference; when the Mac streams faster it scales the box's area by 60/fps (blocks-per-second is linear in frame rate), so a 90/120 Hz session still lands inside the decoder's budget.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The client advertises its decoder ceiling from
MediaCodecInfo.VideoCapabilities.supportedWidths/Heights.upper. That is thesizelimit out of the vendor'smedia_codecs.xml, and vendors routinely overstate it. The constraint that actually binds isblocks-per-second, which that API never reports.Measured on a Galaxy Tab S9 FE (Exynos, 2304x1440 90 Hz panel):
So the client advertised 8192x8192 and the Mac's clamp in
ScreenCapture.encodeSizenever engaged. The real budget at 16x16 blocks is about 1.22M blocks/s:MediaCodec.configure()accepts anything inside 8192x8192. Above the throughput budget the decoder starts, accepts input buffers, and never outputs a frame. That is the black screen in #41. HiDPI made it certain rather than likely, because it doubles both axes.There is a second wall behind that one.
MAX_FRAME_SIZEinStreamClientwas 5MB, and exceeding it throwsIOExceptionand drops the connection. At the higher bitrate presets a keyframe above 1080p clears 5MB, so the same resolution bump could show up as a disconnect instead of a black screen.Fix
Bound the stream by what the client can display and decode, rather than by a number the decoder made up.
CodecCapabilities.maxStreamSize()starts at the panel size and shrinks untilareSizeAndRateSupported()passes. That is the API that consultsblocks-per-second. The panel is the upper bound because the client downscales anything larger on arrival, so decode budget spent above it buys nothing.PanelGeometryreads the true panel fromDisplay.Mode.physicalWidth/Height, which does not change with rotation, and the peak rate acrosssupportedModes.MAX_FRAME_SIZEgoes from 5MB to 32MB. It is only a desync guard.acquireBufferallocates the real frame size, so a generous bound costs nothing in steady state.CodecLimits.clampToClientLimit()transposes the ceiling when the capture's orientation differs from the client's. The limit stands for a macroblock area budget, which does not care which side is longer, so a portrait display measured against a landscape box was losing pixels for no reason.StreamClientrequires aContext. The USB path constructed it without one and skipped the panel probe. The compiler now prevents that.I renamed
maxDecodeSizetonominalMaxDecodeSizeso the trap is visible at the call site.Why this is general
Nothing here keys off a model or vendor. An inflated
sizelimit sitting next to a realblocks-per-secondbudget is howmedia_codecs.xmlnormally reads on Exynos, MediaTek and Qualcomm, so any device whose panel outruns its decoder at the requested frame rate hit this. Devices that report nothing usable keep their old behavior through the existing fallback.Bounding at the panel also makes HiDPI usable on a tablet. macOS renders at 2x, SCStream downsamples to the panel size before encode, and the client decodes a frame it can sustain. That is sharper than a 1x capture of the same size, because it is supersampled.
Verification
Measured on-device before and after, from a side-by-side build:
The 120 fps row is the one I would check first. 2304x1440 at 120 fps needs 1,555,200 blocks/s against a 1,224,000 budget, so it steps down instead of going black.
I also confirmed that HiDPI virtual-display creation was never the broken part. A small test program against
CGVirtualDisplayreproduces2304x1440 logical @ 4608x2880 pixels, backingScaleFactor 2.0correctly. The failure was always downstream, at encode size.macOS builds clean and passes 39/39 tests, including 4 new orientation cases in
CodecLimitsTests. Android compiles clean, unit tests pass, ktlint is clean.A decoder-sizing bug this fixes on the way past
The display-config message is not only what the client's overlay renders. It is also what the client sizes its decoder from:
With HiDPI on,
AppDelegatedeliberately sends the logical resolution so the overlay matches the Mac's dropdown, while the stream is the doubled one. Observed on 0.11.2 with HiDPI at 2560x1600:So the client asks "can you decode 2560x1600 at 90fps", is told yes, picks a decoder on that basis, and is then handed 5120x3200. The only throughput check that exists on the client runs against a resolution that is not the stream. MediaCodec does adapt afterwards from the SPS, but selection and the capability check have already happened by then.
This PR corrects that wherever the clamp engages, through the existing branch at
AppDelegate.swift:656: onceenc != physical, the real encoded size goes out and the decoder is configured for the stream it will receive.One narrow gap remains, and this PR does not close it: HiDPI on a device that genuinely can decode the doubled frame, where the clamp stays inactive and the halved number still goes out. Fixing that properly means always sending the encoded size and carrying the desktop geometry in a separate field for the overlay. That is a protocol change and belongs in its own PR.
Two things I left alone
Both are deliberate choices with UI behind them, so they seemed like yours to make.
The default bitrate of 1000 Mbps sits above the declared
bitrate range 1-80000000(80 Mbps) on this class of decoder. AOSP does not enforce that range inareSizeAndRateSupported, so this fix does not catch it. A lower default, or clamping against the decoder's declared bitrate, would.resetToDefaults()sets 120 Hz, which is higher than many tablet panels can show. Frame rate trades against resolution in the same budget, as the table above shows, so a default above the panel's peak costs resolution without anyone seeing why.